# @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/](https://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/](https://code.foss.global/) account to submit Pull Requests directly.

## Install

```bash
pnpm add @serve.zone/platformclient
```

## Quick Start

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

const platformClient = new SzPlatformClient({
  token: process.env.SERVEZONE_PLATFORM_TOKEN,
  platformUrl: 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 needs an authorization string and a platform endpoint. Authorization comes from explicit options or environment values. Value-free platform bindings can supply endpoint discovery metadata, but never credentials.

| Value | Sources |
| --- | --- |
| Authorization | Constructor string, `authorizationString`, `authorization`, `token`, `init()` argument, `SERVEZONE_PLATFORM_AUTHORIZATION`, or `SERVEZONE_PLATFORM_TOKEN`. |
| Platform URL | `url`, `platformUrl`, `SERVEZONE_PLATFORM_URL`, or the first active binding endpoint using `typedrequest` or `http`. |
| Platform bindings | Constructor `binding` or `bindings`, `SERVEZONE_PLATFORM_BINDING`, or `SERVEZONE_PLATFORM_BINDINGS`. |

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.

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()`.

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

const client = new SzPlatformClient(process.env.SERVEZONE_PLATFORM_AUTHORIZATION);
await client.init();
```

```typescript
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();
```

## 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.

```typescript
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 TypedSocket connections open for platform requests, CoreMail, typed service-mail delivery, and managed Web Push delivery. Call `stop()` when a short-lived process, worker, or test is done with the client.

```typescript
const client = new SzPlatformClient({
  token: process.env.SERVEZONE_PLATFORM_TOKEN,
  platformUrl: 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:

```typescript
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`, and `smtpReady` so applications can distinguish standard TypedSocket mail from explicit SMTP compatibility configuration. 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:

```typescript
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:

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

`registerInboundHandler()` registers this client connection as the typed endpoint for the configured service mail address. dcrouter then delivers inbound messages through the shared `deliverInboundMail` TypedRequest contract.

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

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

Legacy platform-service email request:

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

SMS:

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

Managed Web Push:

```typescript
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:

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

Legacy device-token push:

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

Letter:

```typescript
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:

```typescript
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

`coreMailConnector` is a server-runtime API for standalone CoreMail workload bindings. 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.

```typescript
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.

## 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.

```typescript
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

```bash
pnpm install
pnpm test
pnpm run build
```

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

## License and Legal Information

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

**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<br>
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.
