@serve.zone/platformclient

@serve.zone/platformclient is the application SDK for serve.zone platform services. It opens TypedSocket connections and gives application code focused connectors for CoreMail, transactional email, SMS, push notifications, and physical letters without hand-writing TypedRequest setup.

Issue Reporting and Security

For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.

Install

pnpm add @serve.zone/platformclient

Quick Start

import { SzPlatformClient } from '@serve.zone/platformclient';

const platformClient = new SzPlatformClient({
  token: process.env.SERVEZONE_PLATFORM_TOKEN,
  cloudlyUrl: process.env.SERVEZONE_PLATFORM_URL,
});

await platformClient.init();

await platformClient.emailConnector.sendEmail({
  to: 'user@example.com',
  from: 'hello@example.com',
  title: 'Workspace ready',
  body: 'Your serve.zone workspace is ready.',
});

Connectors

SzPlatformClient owns the shared connection and exposes connector instances:

Connector Main methods Platform capability
emailConnector getServiceMailStatus(), getServiceMailCredentials(), sendMail(), sendText(), sendHtml(), getMailDeliveryStatus(), registerInboundHandler(), normalizeInboundMessage(), handleInboundPayload(), createInboundHandler(), sendEmail() email
smsConnector sendSms(), sendSmsVerifcation() sms
pushNotificationConnector sendPushNotification() pushnotification
webPushConnector getConfigurationStatus(), getWebPushServiceStatus(), getWebPushPublicKey(), enqueueWebPush(), cancelWebPush(), getWebPushDeliveryStatus() pushnotification
letterConnector sendLetter() letter
coreMailConnector prepareOutboundSubmission(), uploadOutboundPart(), finalizeOutboundSubmission(), getOutboundSubmission(), listInboundDeliveries(), fetchInboundDelivery(), acknowledgeInboundDelivery() CoreMail workload binding

The request and response payloads come from @serve.zone/interfaces, so TypeScript stays aligned with the serve.zone platform contracts.

Configuration

The shared platform connection requires a Cloudly machine token and an explicit HTTPS Cloudly origin. On each connection it exchanges the token for an API machine identity and registers that JWT on the physical socket before initialization succeeds. Reconnect repeats both steps, and a live connection renews its identity before expiry. Rejected credentials, invalid registration replies, or failed renewal close the connection. Value-free platform bindings describe capabilities; they never select the Cloudly authority or provide credentials.

Value Sources
Authorization Constructor string, authorizationString, authorization, token, init() argument, SERVEZONE_PLATFORM_AUTHORIZATION, or SERVEZONE_PLATFORM_TOKEN.
Cloudly URL cloudlyUrl, url, platformUrl, or SERVEZONE_PLATFORM_URL; an explicit HTTPS origin is required.
Platform bindings Constructor binding or bindings, SERVEZONE_PLATFORM_BINDING, or SERVEZONE_PLATFORM_BINDINGS.

Workloads should receive their service-scoped platform:session token through SERVEZONE_PLATFORM_AUTHORIZATION. A deployment token does not grant a platform session. The SDK never sends the token to a binding or mail endpoint. Token exchange and registration bypass request payload hooks and disable automatic RPC retries.

Binding environment variables must contain JSON encoded, value-free IPlatformBinding objects from @serve.zone/interfaces.

Service mail credentials come only from workload environment variables. Standard platformclient mail uses dcrouter TypedSocket/TypedRequest service credentials. The default sender uses MAIL_FROM, MAIL_TYPED_URL, MAIL_API_CREDENTIAL_ID, and MAIL_API_CREDENTIAL_SECRET. Additional configured sender addresses use scoped variables such as MAIL_TEST_SERVICE_GATED_ONE_FROM, MAIL_TEST_SERVICE_GATED_ONE_TYPED_URL, MAIL_TEST_SERVICE_GATED_ONE_API_CREDENTIAL_ID, and MAIL_TEST_SERVICE_GATED_ONE_API_CREDENTIAL_SECRET.

Managed Web Push uses a separate, credential-scoped TypedSocket connection configured with WEB_PUSH_TYPED_URL, WEB_PUSH_API_CREDENTIAL_ID, and WEB_PUSH_API_CREDENTIAL_SECRET. These values form one atomic workload environment bundle: all three values are required and partial bundles fail closed. webPushConnector never accepts or sends caller-provided authentication or resource-owner fields; dcrouter derives the service scope from the injected credential.

Service mail can instead be routed through a cluster-scoped CoreMail instance. The default sender uses MAIL_COREMAIL_URL, MAIL_COREMAIL_BINDING_ID, MAIL_COREMAIL_CREDENTIAL_ID, MAIL_COREMAIL_CREDENTIAL_VERSION, and MAIL_COREMAIL_CREDENTIAL_SECRET. Additional sender addresses use scoped MAIL_<TOKEN>_COREMAIL_* variants under the same address token as the other mail variables, for example MAIL_TEST_SERVICE_GATED_ONE_COREMAIL_URL. Cloudly provisions this group for clusters and Onebox for single hosts.

Precedence matches the dcrouter variables: if any scoped MAIL_<TOKEN>_COREMAIL_* value is present, that scoped group is used on its own and the unscoped group is not consulted for that address. The group is atomic — a partially configured group fails closed with the exact missing variable names rather than silently falling back to dcrouter. When a CoreMail group resolves for the selected address, sendMail(), sendText(), sendHtml(), getMailDeliveryStatus(), and registerInboundHandler() use CoreMail; otherwise they use the dcrouter MAIL_TYPED_URL path, exactly as before. MAIL_COREMAIL_URL must be one canonical https://.../socket URL; http:// and ws:// are accepted only for loopback hosts, mirroring the rule TypedSocket itself enforces.

Each distinct binding gets its own connector instance, so a service may hold an unscoped group and several scoped groups at once and send from any of them.

SMTP compatibility credentials are still discoverable with getServiceMailCredentials() for applications that explicitly implement SMTP themselves. Those compatibility variables are SMTP_HOST, SMTP_PORT, SMTP_TLS_MODE, SMTP_USERNAME, and SMTP_PASSWORD, plus scoped MAIL_<TOKEN>_SMTP_* variants. They are not used by sendMail(), sendText(), sendHtml(), or registerInboundHandler().

import { SzPlatformClient } from '@serve.zone/platformclient';

const client = new SzPlatformClient(process.env.SERVEZONE_PLATFORM_AUTHORIZATION);
await client.init();
import { SzPlatformClient } from '@serve.zone/platformclient';

const client = new SzPlatformClient({
  authorization: process.env.SERVEZONE_PLATFORM_AUTHORIZATION,
  url: process.env.SERVEZONE_PLATFORM_URL,
});

await client.init();

Socket 8 migration

Version 4 requires Socket 8 peers: upgrade Cloudly and the dedicated dcrouter mail/Web Push endpoints together with their clients. Older Socket 4/5 servers are incompatible. Set SERVEZONE_PLATFORM_URL to Cloudly's HTTPS origin and retain MAIL_TYPED_URL for the dedicated mail endpoint. Replace legacy platform tokens with Cloudly service credentials; initialization no longer accepts a successful transport handshake as authentication. CoreMail, SMTP credential discovery, mail delivery status, attachments, Reply-To, and debug connector behavior remain available.

Debug Mode

Pass test as the authorization string to activate debug mode for the shared platform connection and the legacy service connectors. Those connectors log or return deterministic test values instead of sending real platform requests. CoreMail has no debug transport: omit CoreMail configuration in debug clients, because calling coreMailConnector methods still requires normal workload authentication.

const client = new SzPlatformClient('test');
await client.init();

await client.emailConnector.sendEmail({
  to: 'developer@example.com',
  from: 'hello@example.com',
  title: 'Preview only',
  body: 'This message is logged, not sent.',
});

const verificationCode = await client.smsConnector.sendSmsVerifcation({
  toNumber: 491234567890,
  fromName: 'ServeZone',
});

console.log(verificationCode); // 123456

The current SMS verification method is spelled sendSmsVerifcation() in code. Use that exact method name until the public API changes.

Lifecycle

SzPlatformClient keeps separate TypedSocket connections for Cloudly, CoreMail, typed service mail, and managed Web Push. Call stop() to cancel initialization, renewal, and restoration and join their cleanup. requireTypedSocket() returns the authenticated Cloudly connection while its identity is valid; typedsocket is optional. The mail router is private to its own connection, so Cloudly and Web Push peers cannot invoke inbound mail handlers.

const client = new SzPlatformClient({
  token: process.env.SERVEZONE_PLATFORM_TOKEN,
  cloudlyUrl: process.env.SERVEZONE_PLATFORM_URL,
});

await client.init();

try {
  await client.emailConnector.sendText({
    to: 'user@example.com',
    subject: 'Done',
    text: 'The job has finished.',
  });
} finally {
  await client.stop();
}

Connector Examples

Service mail over managed TypedSocket credentials:

const status = await client.emailConnector.getServiceMailStatus();

if (status.ready) {
  const sendResult = await client.emailConnector.sendText({
    to: 'user@example.com',
    replyTo: 'customer@example.com',
    subject: 'Service mail ready',
    text: 'This message was sent through the managed service mail identity.',
  });

  if (sendResult.spoolItemId) {
    const delivery = await client.emailConnector.getMailDeliveryStatus(sendResult.spoolItemId);
    console.log(delivery.spoolItem?.status);
  }
}

getServiceMailStatus().ready means the typed service-mail path is configured. The status also exposes transport, typedUrl, typedCredentialConfigured, coreMailConfigured, coreMailEndpointUrl, and smtpReady so applications can distinguish CoreMail, standard TypedSocket mail, and explicit SMTP compatibility configuration. transport is 'coremail' when a CoreMail group resolves, 'typedsocket' for the dcrouter path, 'smtp' when only SMTP compatibility values are present, and 'none' otherwise. A partially configured CoreMail group reports ready: false with the exact missing MAIL_COREMAIL_* names in missing. Send helpers return spoolItemId, which can be passed to getMailDeliveryStatus() to inspect accepted, queued, deferred, delivered, or failed delivery state. Set replyTo to one bare mailbox address when replies should go somewhere other than the managed sender. Dcrouter validates that field and renders the authoritative Reply-To header; do not supply Reply-To through the free-form headers object.

Specific sender address:

await client.emailConnector.sendHtml({
  from: 'test-service@gated.one',
  to: 'user@example.com',
  subject: 'Platform capability test',
  html: '<p>The service mail identity is working.</p>',
});

Typed inbound mail registration:

await client.emailConnector.registerInboundHandler(async (message) => {
  console.log(message.from, message.to, message.subject);
  return { accepted: true, workAppMessageId: message.messageId };
});

registerInboundHandler() behaves according to the resolved transport. On the dcrouter path it registers the dedicated mail connection as the typed endpoint for the configured service mail address, and dcrouter then delivers inbound messages through deliverInboundMail. When a CoreMail group resolves for that address, CoreMail is pull-based instead: the connector that owns the address's binding runs a bounded polling loop that lists pending deliveries, fetches each MIME body, invokes the handler with the same message shape the dcrouter path produces, and acknowledges the delivery as processed only after the handler resolves. A handler that throws, or returns { accepted: false }, leaves the delivery pending for a later tick. The loop starts on first registration, never overlaps ticks, backs off on transport failure, and is stopped by stop(). Tune it with setCoreMailInboundPollOptions({ intervalMs, pageLimit }) before registering; the default is one tick every 5000 ms over 25 deliveries, with a floor of 250 ms. After reconnect, exact credential-scoped endpoint registration completes before the mail connection becomes ready. A denied or malformed registration prevents readiness.

Inbound message normalization for webhook or direct TypedRequest-style delivery:

const inboundHandler = client.emailConnector.createInboundHandler(async (message) => {
  console.log(message.from, message.to, message.subject);
  return { accepted: true };
});

Legacy platform-service email request:

await client.emailConnector.sendEmail({
  to: 'user@example.com',
  from: 'hello@example.com',
  title: 'Invoice ready',
  body: 'Your invoice is available in the dashboard.',
});

SMS:

const status = await client.smsConnector.sendSms({
  toNumber: 491234567890,
  fromName: 'ServeZone',
  messageText: 'Your code is 123456.',
});

Managed Web Push:

const serviceStatus = await client.webPushConnector.getWebPushServiceStatus();
if (!serviceStatus.ready || !serviceStatus.activeVapidKey) {
  throw new Error(serviceStatus.message || 'Web Push is not ready');
}
const activeVapidKey = serviceStatus.activeVapidKey;
const applicationServerKey = activeVapidKey.publicKey;

const browserSubscription = await serviceWorkerRegistration.pushManager.subscribe({
  userVisibleOnly: true,
  applicationServerKey,
});

const enqueueResult = await client.webPushConnector.enqueueWebPush({
  idempotencyKey: 'notification-delivery-42',
  subscriptionId: 'opaque-application-subscription-id',
  subscription: browserSubscription.toJSON(),
  vapidKeyId: activeVapidKey.id,
  payload: {
    schemaVersion: 1,
    event: 'notificationAvailable',
    eventId: 'notification-event-42',
    route: '/notifications',
  },
  ttlSeconds: 300,
  urgency: 'normal',
  collapseKey: 'notification-inbox',
});

if (enqueueResult.spoolItemId) {
  const delivery = await client.webPushConnector.getWebPushDeliveryStatus(
    enqueueResult.spoolItemId,
  );
  console.log(delivery?.state);
}

The active VAPID key ID and public key come from one service-status snapshot, so a rotation cannot mismatch the subscription key and enqueue key ID. Store browser subscriptions as sensitive application data and only send the privacy-minimal notificationAvailable payload shown above. enqueueWebPush().accepted means dcrouter accepted the item into its delivery spool; it does not prove browser receipt or display. A terminal pushServiceAccepted state means the remote push service accepted the encrypted request.

Pending delivery can be cancelled by spool item or by opaque application subscription ID:

await client.webPushConnector.cancelWebPush({
  type: 'subscription',
  subscriptionId: 'opaque-application-subscription-id',
});

Legacy device-token push:

const status = await client.pushNotificationConnector.sendPushNotification({
  deviceToken: 'device-token-from-your-app',
  message: 'Deployment complete: your service is live.',
});

Letter:

await client.letterConnector.sendLetter({
  description: 'Important account information',
  needsCover: true,
  title: 'Account update',
  coverBody: 'This letter was generated through serve.zone.',
  service: ['Einschreiben'],
});

Service-mail and managed Web Push helper types are exported from @serve.zone/platformclient. IServiceMailSendResult.spoolItemId identifies the dcrouter delivery spool item for follow-up status queries through getMailDeliveryStatus(spoolItemId, address?). IServiceWebPushEnqueueOptions and TServiceWebPushCancellationTarget mirror the credential-scoped Web Push delivery contract without exposing auth or owner fields. The legacy platform-service request fields remain under platform.email, platform.sms, platform.pushnotification, and platform.letter.

Dcrouter rejections with a stable submission code are exposed as ServiceMailSubmissionError. Uncoded rejections remain plain Error instances:

import { ServiceMailSubmissionError } from '@serve.zone/platformclient';

try {
  await client.emailConnector.sendText({
    to: 'user@example.com',
    replyTo: 'customer@example.com',
    subject: 'Service mail',
    text: 'Hello',
  });
} catch (error) {
  if (error instanceof ServiceMailSubmissionError) {
    console.error(error.code);
  }
  throw error;
}

The exported TServiceMailSubmissionErrorCode type contains the supported dcrouter rejection codes.

CoreMail

There are two ways to reach CoreMail, and most applications want the first.

As the service-mail transport. Set the MAIL_COREMAIL_* group described under Configuration and keep using emailConnector: sendMail(), getServiceMailStatus(), getMailDeliveryStatus(), and registerInboundHandler() all route through CoreMail automatically, with the same IServiceMailSendOptions input as the dcrouter path. No CoreMail-specific application code is required.

import { SzPlatformClient } from '@serve.zone/platformclient';

const client = new SzPlatformClient();
await client.init(); // reads MAIL_COREMAIL_* from the workload environment

const sent = await client.emailConnector.sendMail({
  to: 'recipient@example.com',
  subject: 'Sent through CoreMail',
  text: 'Bodies and attachments become uploaded parts automatically.',
});

const status = await client.emailConnector.getMailDeliveryStatus(sent.spoolItemId!);

Bodies, HTML, and attachments become CoreMail parts with client-computed SHA-256 digests and lengths, uploaded and finalized for you. Contract limits (1 MiB text, 2 MiB HTML, 16 attachments, 10 MiB each, 17 MiB aggregate, 100 recipients) are enforced before anything is sent. idempotencyKey is passed through; CoreMail requires one, so a fresh key is generated when the caller omits it, which reproduces the dcrouter semantics of an unkeyed send. CoreMail error codes that TMailSubmissionErrorCode also carries surface unchanged on ServiceMailSubmissionError.code; codes with no counterpart become a generic Error carrying the code.

As a direct workload API. coreMailConnector is a server-runtime API for standalone CoreMail workload bindings, for applications that manage submissions or inbound deliveries themselves. It is not a browser or cross-origin API. Pass the injected workload authority explicitly; the connector never reads credential material from platform-binding metadata. The endpoint must be one canonical https://.../socket URL, or an http:///ws:// loopback URL for local development.

import { SzPlatformClient } from '@serve.zone/platformclient';

// APP_* names are deployment-owned environment keys in this example.
const runtimeCoreMail = {
  endpointUrl: process.env.APP_COREMAIL_ENDPOINT_URL!,
  bindingId: process.env.APP_COREMAIL_BINDING_ID!,
  credentialId: process.env.APP_COREMAIL_CREDENTIAL_ID!,
  credentialVersion: Number(process.env.APP_COREMAIL_CREDENTIAL_VERSION),
  credentialSecret: process.env.APP_COREMAIL_CREDENTIAL_SECRET!,
};

const client = new SzPlatformClient({
  coreMail: {
    endpointUrl: runtimeCoreMail.endpointUrl,
    bindingId: runtimeCoreMail.bindingId,
    credentialId: runtimeCoreMail.credentialId,
    credentialVersion: runtimeCoreMail.credentialVersion,
    credentialSecret: runtimeCoreMail.credentialSecret,
  },
});

await client.init();

try {
  const page = await client.coreMailConnector.listInboundDeliveries({
    limit: 25,
  });

  for (const delivery of page.deliveries) {
    const fetched = await client.coreMailConnector.fetchInboundDelivery(delivery);
    await processMimeMessage(fetched.bytes);
    await client.coreMailConnector.acknowledgeInboundDelivery({
      deliveryId: delivery.deliveryId,
      outcome: 'processed',
    });
  }
} finally {
  await client.stop();
}

Outbound delivery is descriptor-first: call prepareOutboundSubmission(), upload every declared byte sequence with uploadOutboundPart(), then call finalizeOutboundSubmission(). Upload and download grants are path-only, same-origin, one-time capabilities. The connector validates the authenticated operation set, grant context, transfer headers, byte length, and SHA-256 digest before completing a transfer. A general platform authorization string is not required when the client is configured only for CoreMail.

Manual polling as shown above is only needed when driving coreMailConnector directly. Applications using CoreMail as the service-mail transport should call emailConnector.registerInboundHandler() instead and let the connector own the loop.

configure() is one-shot per connector instance. When several bindings are configured through the MAIL_COREMAIL_* groups, emailConnector keeps one connector per binding — reusing client.coreMailConnector for the first or matching binding and creating dedicated instances for the rest — and stops the ones it created in stop().

Platform Bindings

Platform bindings allow a workload to discover value-free endpoint and capability metadata from its runtime environment. Secret values are injected separately as workload environment variables or explicit connector options.

import { SzPlatformClient } from '@serve.zone/platformclient';
import { platform } from '@serve.zone/interfaces';

const binding: platform.IPlatformBinding = JSON.parse(
  process.env.SERVEZONE_PLATFORM_BINDING!
);

const client = new SzPlatformClient({
  authorization: process.env.SERVEZONE_PLATFORM_AUTHORIZATION,
  binding,
});
await client.init();

const emailBinding = client.getPlatformBinding('email');

Bindings with desiredState: 'disabled' or status: 'failed' are ignored when the client auto-selects an endpoint.

InfoHtml Helper

The repository also contains a small ts_infohtml source folder for rendering simple informational HTML pages from text or options. It is not exported from the package root, so treat it as a source-level helper rather than the main SDK API.

Development

pnpm install
pnpm test
pnpm run build

The package is authored as ESM TypeScript and builds source folders with tsbuild tsfolders --web --allowimplicitany.

This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the repository license file.

Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.

Trademarks

This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.

Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.

Company Information

Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany

For any legal inquiries or further information, please contact us via email at hello@task.vc.

By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

S
Description
Application SDK for authenticated serve.zone platform connections, service mail, and Web Push.
Readme
1.4 MiB
Languages
TypeScript 100%