@byok-sdk/cloud 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +9 -0
  3. package/dist/auth/bearer.d.ts +25 -0
  4. package/dist/auth/device-proof.d.ts +38 -0
  5. package/dist/auth/plane.d.ts +67 -0
  6. package/dist/auth/tokens.d.ts +37 -0
  7. package/dist/auth/verify.d.ts +22 -0
  8. package/dist/board-projection.d.ts +9 -0
  9. package/dist/capabilities.d.ts +71 -0
  10. package/dist/cloud.d.ts +123 -0
  11. package/dist/composition/in-memory.d.ts +55 -0
  12. package/dist/coordination-client.d.ts +87 -0
  13. package/dist/coordination.d.ts +29 -0
  14. package/dist/crypto/port.d.ts +44 -0
  15. package/dist/crypto/web-crypto.d.ts +28 -0
  16. package/dist/errors.d.ts +45 -0
  17. package/dist/handlers/auth.d.ts +21 -0
  18. package/dist/handlers/blobs.d.ts +39 -0
  19. package/dist/handlers/board.d.ts +21 -0
  20. package/dist/handlers/capabilities.d.ts +14 -0
  21. package/dist/handlers/events.d.ts +33 -0
  22. package/dist/handlers/messages.d.ts +28 -0
  23. package/dist/handlers/presence.d.ts +13 -0
  24. package/dist/handlers/shared.d.ts +30 -0
  25. package/dist/handlers/truth.d.ts +15 -0
  26. package/dist/inbound.d.ts +35 -0
  27. package/dist/index.d.ts +57 -0
  28. package/dist/index.js +2419 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/router/registry.d.ts +56 -0
  31. package/dist/stores/in-memory/blobs.d.ts +89 -0
  32. package/dist/stores/in-memory/dedup.d.ts +17 -0
  33. package/dist/stores/in-memory/device-directory.d.ts +19 -0
  34. package/dist/stores/in-memory/index.d.ts +36 -0
  35. package/dist/stores/in-memory/nonces.d.ts +22 -0
  36. package/dist/stores/in-memory/pairing-codes.d.ts +18 -0
  37. package/dist/stores/in-memory/proof-receipts.d.ts +11 -0
  38. package/dist/stores/in-memory/rate-limiter.d.ts +13 -0
  39. package/dist/stores/in-memory/receipts.d.ts +21 -0
  40. package/dist/stores/in-memory/sequence.d.ts +11 -0
  41. package/dist/stores/in-memory/task-attempts.d.ts +36 -0
  42. package/dist/stores/ports-contract.d.ts +20 -0
  43. package/dist/stores/ports.d.ts +309 -0
  44. package/dist/tenant-stores.d.ts +124 -0
  45. package/dist/truth/contract.d.ts +149 -0
  46. package/dist/truth/errors.d.ts +8 -0
  47. package/package.json +52 -0
@@ -0,0 +1,56 @@
1
+ /**
2
+ * The route inventory (sprint I1).
3
+ *
4
+ * Isolation review starts by asking "what routes exist, and what does each one
5
+ * require?" — a question that is only answerable if the answer cannot drift
6
+ * from what is actually mounted. So mounting goes through {@link
7
+ * CloudRouteRegistry.register} and nowhere else: the registry owns the Hono
8
+ * app, hands out no reference to it that could be mounted on directly, and
9
+ * records a class for every route as it goes.
10
+ *
11
+ * `src/__tests__/route-inventory.test.ts` closes that loop in BOTH directions
12
+ * against Hono's own `app.routes` table: a mounted route missing from the
13
+ * inventory fails the suite, and an inventoried route that never reached the
14
+ * router fails it too. A route whose class is not one of {@link ROUTE_CLASSES}
15
+ * cannot be registered at all.
16
+ */
17
+ import { type Context } from 'hono';
18
+ /**
19
+ * What a route requires of its caller. Not a description of the handler's
20
+ * work — a description of the credential it is authenticated by, which is the
21
+ * thing an isolation review has to enumerate:
22
+ *
23
+ * - `device` — a bearer access token; resolves to a `DevicePrincipal` and a
24
+ * tenant-closed facade.
25
+ * - `proof` — a request-bound Ed25519 device proof; tenant/product/key
26
+ * authority comes from the current device row, not protected claims.
27
+ * - `presigned` — no principal at all; an HMAC signature over the resource id
28
+ * plus an expiry IS the credential (§7's two `/content` routes).
29
+ * - `public` — deliberately unauthenticated; must expose nothing tenant-scoped.
30
+ */
31
+ export declare const ROUTE_CLASSES: readonly ['device', 'proof', 'presigned', 'public'];
32
+ export type RouteClass = (typeof ROUTE_CLASSES)[number];
33
+ export declare const ROUTE_METHODS: readonly ['GET', 'POST', 'PUT'];
34
+ export type RouteMethod = (typeof ROUTE_METHODS)[number];
35
+ export interface RouteDescriptor {
36
+ readonly method: RouteMethod;
37
+ readonly path: string;
38
+ readonly class: RouteClass;
39
+ }
40
+ export type CloudRouteHandler = (c: Context) => Response | Promise<Response>;
41
+ export declare function routeKey(route: {
42
+ readonly method: string;
43
+ readonly path: string;
44
+ }): string;
45
+ export declare class CloudRouteRegistry {
46
+ #private;
47
+ register(descriptor: RouteDescriptor, handler: CloudRouteHandler): void;
48
+ /** The inventory, in registration order. */
49
+ get routes(): readonly RouteDescriptor[];
50
+ /** What the router actually mounted, read back off Hono itself — the other half of the I1 comparison. */
51
+ get mounted(): readonly {
52
+ readonly method: string;
53
+ readonly path: string;
54
+ }[];
55
+ get fetch(): (request: Request, ...rest: unknown[]) => Response | Promise<Response>;
56
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * The in-memory blob pair: metadata and bytes in process memory, HMAC-signed
3
+ * expiring URLs (docs/protocol.md §7).
4
+ *
5
+ * TWO objects, one shared record map, because the contract they satisfy is two
6
+ * things: {@link CloudBlobStore} is the tenant-first port every composition
7
+ * owes, and {@link BlobContentProxy} is the optional byte-carrying half. The
8
+ * in-memory composition is the one that supplies both — it has nowhere else to
9
+ * put bytes — which is why hosted-in-memory behavior is byte-for-byte what it
10
+ * was before the split.
11
+ *
12
+ * They cannot be one object: the port's method inventory is contract data
13
+ * (`CLOUD_PORT_METHODS`) and `@byok-sdk/conformance` asserts a composition's blob
14
+ * store implements EXACTLY the two declared methods. An object carrying all
15
+ * six would fail that assertion, which is the point — the suite is what keeps
16
+ * the narrowed port narrow.
17
+ *
18
+ * The presigned URL form — `/byok/blobs/<id>/content?sig=&exp=` — is the
19
+ * relative shape the daemon's blob client already resolves against its server
20
+ * base, so a hosted deployment is indistinguishable from a self-hosted one on
21
+ * this route too.
22
+ *
23
+ * Tenancy: `createUpload` records the owning tenant and reservation while
24
+ * `observeUpload`/`getDownloadUrl` refuse any other, so the three bearer routes are
25
+ * tenant-closed. The two `/content` routes are presigned by construction and
26
+ * have no principal at all — the signature over the blob id IS the credential
27
+ * (same posture as the reference server, §7).
28
+ */
29
+ import { type Clock, type ContentHash, type ObjectStore, type StorageReservation, type TenantId } from '@byok-sdk/core';
30
+ import type { CloudCrypto } from '../../crypto/port';
31
+ import type { BlobContent, BlobContentProxy, BlobObservation, BlobWriteResult, CloudBlobStore } from '../ports';
32
+ /** How long a presigned upload/download URL stays valid. */
33
+ export declare const BLOB_URL_TTL_MS: number;
34
+ interface BlobRecord {
35
+ readonly tenantId: TenantId;
36
+ readonly reservationId: string;
37
+ readonly contentHash: ContentHash;
38
+ readonly byteSize: bigint;
39
+ readonly contentType: string;
40
+ uploaded: boolean;
41
+ data?: Uint8Array;
42
+ }
43
+ export interface InMemoryBlobStoreOptions {
44
+ readonly urlTtlMs?: number;
45
+ }
46
+ /**
47
+ * The state both halves read, and the only thing they share.
48
+ *
49
+ * Module-private on purpose: it is not a port, and nothing outside this file
50
+ * should be able to reach a blob record without going through one of the two
51
+ * contracts above.
52
+ */
53
+ declare class InMemoryBlobRegistry {
54
+ readonly blobs: Map<string, BlobRecord>;
55
+ readonly reservationBlobs: Map<string, string>;
56
+ readonly clock: Clock;
57
+ readonly crypto: CloudCrypto;
58
+ readonly secret: Uint8Array;
59
+ readonly urlTtlMs: number;
60
+ constructor(clock: Clock, crypto: CloudCrypto, options: InMemoryBlobStoreOptions);
61
+ signUrl(blobId: string, action: 'put' | 'get'): Promise<string>;
62
+ computeSig(blobId: string, action: 'put' | 'get', exp: number): Promise<string>;
63
+ }
64
+ /** The narrowed port: tenant-first grants, no bytes. */
65
+ export declare class InMemoryCloudBlobStore implements CloudBlobStore {
66
+ #private;
67
+ constructor(registry: InMemoryBlobRegistry, objects: ObjectStore);
68
+ createUpload(tenant: TenantId, reservation: StorageReservation): Promise<{
69
+ blobId: string;
70
+ uploadUrl: string;
71
+ }>;
72
+ observeUpload(tenant: TenantId, blobId: string, reservation: StorageReservation): Promise<BlobObservation | undefined>;
73
+ getDownloadUrl(tenant: TenantId, blobId: string): Promise<string | undefined>;
74
+ }
75
+ /** The optional half: the bytes this composition has nowhere else to put. */
76
+ export declare class InMemoryBlobContentProxy implements BlobContentProxy {
77
+ #private;
78
+ constructor(registry: InMemoryBlobRegistry);
79
+ verifySignedUrl(blobId: string, action: 'put' | 'get', sig: string, exp: number): Promise<boolean>;
80
+ writeContent(blobId: string, data: Uint8Array): Promise<BlobWriteResult>;
81
+ readContent(blobId: string): Promise<BlobContent | undefined>;
82
+ }
83
+ /** Both halves over one registry. The only way to obtain either. */
84
+ export interface InMemoryBlobs {
85
+ readonly blobs: CloudBlobStore;
86
+ readonly contentProxy: BlobContentProxy;
87
+ }
88
+ export declare function createInMemoryBlobs(clock: Clock, crypto: CloudCrypto, objects: ObjectStore, options?: InMemoryBlobStoreOptions): InMemoryBlobs;
89
+ export {};
@@ -0,0 +1,17 @@
1
+ /**
2
+ * In-memory {@link InboundDedupStore} (N3).
3
+ *
4
+ * A bounded ring per (tenant, device), not an unbounded set: the wire is
5
+ * at-least-once (§9), so this makes processing at-most-once without letting a
6
+ * chatty device grow memory without limit. Check-and-record is one call, so a
7
+ * composition cannot accidentally split it into a racy read-then-write.
8
+ */
9
+ import { type TenantId } from '@byok-sdk/core';
10
+ import type { InboundDedupStore } from '../ports';
11
+ /** Ids retained per device. Same order of magnitude as the reference server's ring. */
12
+ export declare const DEDUP_RING_CAPACITY = 1024;
13
+ export declare class InMemoryInboundDedupStore implements InboundDedupStore {
14
+ #private;
15
+ constructor(capacity?: number);
16
+ checkAndRecord(tenant: TenantId, deviceId: string, envelopeId: string): Promise<boolean>;
17
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * In-memory {@link DeviceDirectory}.
3
+ *
4
+ * Rows live under a `(tenant, deviceId)` composite key, so a cross-tenant read
5
+ * is not "denied" — it addresses a different key space and finds nothing
6
+ * (§12.6.2 layer 3). The pre-tenant index below holds the SAME record objects,
7
+ * so a revocation applied through the composite key is immediately visible to
8
+ * `/byok/challenge` and `/byok/token` with no second copy to keep in sync.
9
+ */
10
+ import { type TenantId } from '@byok-sdk/core';
11
+ import type { DeviceDirectory, DeviceRecord, DeviceRegistration } from '../ports';
12
+ export declare class InMemoryDeviceDirectory implements DeviceDirectory {
13
+ #private;
14
+ register(tenant: TenantId, input: DeviceRegistration): Promise<DeviceRecord>;
15
+ get(tenant: TenantId, deviceId: string): Promise<DeviceRecord | undefined>;
16
+ revoke(tenant: TenantId, deviceId: string): Promise<void>;
17
+ list(tenant: TenantId): Promise<readonly DeviceRecord[]>;
18
+ resolveByDeviceId(deviceId: string): Promise<DeviceRecord | undefined>;
19
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The in-memory reference implementation of every cloud-local port.
3
+ *
4
+ * Same posture as `@byok-sdk/core`'s: a reference, not a production store. What it
5
+ * guarantees is that the behavior the handler suites assert is achievable
6
+ * without a database — which is what makes those same assertions meaningful
7
+ * when a durable composition (S3b's journal, S4A's schema) runs them later.
8
+ */
9
+ import type { Clock, ObjectStore } from '@byok-sdk/core';
10
+ import type { CloudCrypto } from '../../crypto/port';
11
+ import type { BlobContentProxy, CloudStores } from '../ports';
12
+ export { AllowAllRateLimiter } from './rate-limiter';
13
+ export { BLOB_URL_TTL_MS, InMemoryBlobContentProxy, InMemoryCloudBlobStore, createInMemoryBlobs, } from './blobs';
14
+ export type { InMemoryBlobs, InMemoryBlobStoreOptions } from './blobs';
15
+ export { DEDUP_RING_CAPACITY, InMemoryInboundDedupStore } from './dedup';
16
+ export { InMemoryDeviceDirectory } from './device-directory';
17
+ export { InMemoryDeviceSequenceStore } from './sequence';
18
+ export { InMemoryNonceStore, NONCE_TTL_MS } from './nonces';
19
+ export { InMemoryPairingCodeStore } from './pairing-codes';
20
+ export { InMemoryRequestReceiptStore } from './receipts';
21
+ export { InMemoryProofRequestReceiptStore } from './proof-receipts';
22
+ export { InMemoryTaskAttemptStore } from './task-attempts';
23
+ /**
24
+ * The port bundle plus the byte proxy, in the shape `createInMemoryCoreStores`
25
+ * already uses: the composition is an object with a `stores` field, not the
26
+ * bundle itself.
27
+ *
28
+ * `blobContentProxy` sits BESIDE `stores` rather than inside it because it is
29
+ * not a port — `createByokCloud` takes it as its own optional input, and a
30
+ * composition that cannot carry bytes simply has none to hand over.
31
+ */
32
+ export interface InMemoryCloudComposition {
33
+ readonly stores: CloudStores;
34
+ readonly blobContentProxy: BlobContentProxy;
35
+ }
36
+ export declare function createInMemoryCloudStores(clock: Clock, crypto: CloudCrypto, objects: ObjectStore): InMemoryCloudComposition;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * In-memory {@link NonceStore}: single-use challenge nonces, ~5min TTL
3
+ * (docs/protocol.md §6.2).
4
+ *
5
+ * A nonce is bound to the (tenant, device) it was issued for, so a nonce
6
+ * issued to one tenant's device is not validatable by another's even if the
7
+ * value leaks.
8
+ */
9
+ import { type Clock, type TenantId } from '@byok-sdk/core';
10
+ import type { CloudCrypto } from '../../crypto/port';
11
+ import type { NonceStore } from '../ports';
12
+ /** ~5min, matching the reference server (docs/protocol.md §6.2). */
13
+ export declare const NONCE_TTL_MS: number;
14
+ export declare class InMemoryNonceStore implements NonceStore {
15
+ #private;
16
+ constructor(clock: Clock, crypto: CloudCrypto, ttlMs?: number);
17
+ /** Number of records currently held (post-sweep). Test-facing only. */
18
+ get size(): number;
19
+ issue(tenant: TenantId, deviceId: string): Promise<string>;
20
+ validate(tenant: TenantId, deviceId: string, nonce: string): Promise<boolean>;
21
+ markUsed(tenant: TenantId, nonce: string): Promise<void>;
22
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * In-memory {@link PairingCodeStore}: single-use codes bound to the tenant and
3
+ * product they were minted for.
4
+ *
5
+ * `redeem` answers `undefined` for unknown, expired, and already-used alike.
6
+ * The reference server distinguishes those three in its 401 text; a hosted,
7
+ * multi-tenant surface deliberately does not — the code is a bearer credential
8
+ * addressable across every tenant, and "already used" versus "never existed"
9
+ * is exactly the difference an attacker enumerating codes would pay for.
10
+ */
11
+ import type { Clock, TenantId } from '@byok-sdk/core';
12
+ import type { PairingCodeClaims, PairingCodeInfo, PairingCodeIssueInput, PairingCodeStore } from '../ports';
13
+ export declare class InMemoryPairingCodeStore implements PairingCodeStore {
14
+ #private;
15
+ constructor(clock: Clock);
16
+ issue(tenant: TenantId, input: PairingCodeIssueInput): Promise<PairingCodeInfo>;
17
+ redeem(code: string): Promise<PairingCodeClaims | undefined>;
18
+ }
@@ -0,0 +1,11 @@
1
+ import { type Clock, type TenantId } from '@byok-sdk/core';
2
+ import type { ProofRequestReceipt, ProofRequestReceiptInput, ProofRequestReceiptStore } from '../ports';
3
+ export declare class InMemoryProofRequestReceiptStore implements ProofRequestReceiptStore {
4
+ #private;
5
+ constructor(clock: Clock);
6
+ record(tenant: TenantId, input: ProofRequestReceiptInput): Promise<{
7
+ readonly receipt: ProofRequestReceipt;
8
+ readonly created: boolean;
9
+ }>;
10
+ get(tenant: TenantId, deviceId: string, requestId: string): Promise<ProofRequestReceipt | undefined>;
11
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The reference {@link InboundRateLimiter}: allow-all.
3
+ *
4
+ * S3a's job is to put the seam at gate position 0 — before the type-allow
5
+ * check — not to invent a limiter policy. A hosted deployment's real budget
6
+ * lives at its edge; when it arrives it plugs in here and the gate order does
7
+ * not move.
8
+ */
9
+ import type { TenantId } from '@byok-sdk/core';
10
+ import type { InboundRateLimiter } from '../ports';
11
+ export declare class AllowAllRateLimiter implements InboundRateLimiter {
12
+ consume(_tenant: TenantId, _deviceId: string): Promise<boolean>;
13
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * In-memory {@link RequestReceiptStore}: first write wins.
3
+ *
4
+ * The terminal a device reports is a fact, and a retry of the same terminal
5
+ * (the wire is at-least-once) must not overwrite the first one — `created:
6
+ * false` is how the caller learns it was a replay rather than a new fact.
7
+ */
8
+ import { type Clock, type TenantId } from '@byok-sdk/core';
9
+ import type { RequestReceipt, RequestReceiptStore } from '../ports';
10
+ export declare class InMemoryRequestReceiptStore implements RequestReceiptStore {
11
+ #private;
12
+ constructor(clock: Clock);
13
+ record(tenant: TenantId, input: {
14
+ key: string;
15
+ body: string;
16
+ }): Promise<{
17
+ receipt: RequestReceipt;
18
+ created: boolean;
19
+ }>;
20
+ get(tenant: TenantId, key: string): Promise<RequestReceipt | undefined>;
21
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * In-memory {@link DeviceSequenceStore}: a monotonic per-(tenant, device)
3
+ * delivery counter starting at 1, matching what a mailbox numbers its first
4
+ * row.
5
+ */
6
+ import { type TenantId } from '@byok-sdk/core';
7
+ import type { DeviceSequenceStore } from '../ports';
8
+ export declare class InMemoryDeviceSequenceStore implements DeviceSequenceStore {
9
+ #private;
10
+ next(tenant: TenantId, deviceId: string): Promise<number>;
11
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * In-memory {@link TaskAttemptStore} — the ownership authority the inbound
3
+ * gate reads (N2).
4
+ *
5
+ * Two deliberate no-ops:
6
+ *
7
+ * - `claim` on a task this tenant never offered writes nothing. A device that
8
+ * guesses a taskId must not be able to conjure a row (and, cross-tenant,
9
+ * must not leave a trace in the tenant it guessed into).
10
+ * - `recordStatus` on an unknown task writes nothing, mirroring the reference
11
+ * server's per-type handlers, whose behavior on a missing record is a no-op
12
+ * rather than a rejection.
13
+ *
14
+ * Ownership is first-claim-wins and never transfers: reassigning an owner is
15
+ * the one operation that would make the gate's cross-device assertion
16
+ * unfalsifiable.
17
+ */
18
+ import { type Clock, type TenantId } from '@byok-sdk/core';
19
+ import type { TaskAttempt, TaskAttemptStatus, TaskAttemptStore } from '../ports';
20
+ export declare class InMemoryTaskAttemptStore implements TaskAttemptStore {
21
+ #private;
22
+ constructor(clock: Clock);
23
+ open(tenant: TenantId, input: {
24
+ taskId: string;
25
+ deviceId: string;
26
+ }): Promise<TaskAttempt>;
27
+ get(tenant: TenantId, taskId: string): Promise<TaskAttempt | undefined>;
28
+ claim(tenant: TenantId, input: {
29
+ taskId: string;
30
+ deviceId: string;
31
+ }): Promise<TaskAttempt | undefined>;
32
+ recordStatus(tenant: TenantId, input: {
33
+ taskId: string;
34
+ status: TaskAttemptStatus;
35
+ }): Promise<TaskAttempt | undefined>;
36
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The declared method inventory of every cloud-local port.
3
+ *
4
+ * The exact counterpart of `@byok-sdk/core`'s `ports-contract.ts`, and it exists
5
+ * for the same reason: the table says what a port IS, so it has to be readable
6
+ * by every enforcer without any of them owning it. `@byok-sdk/conformance` asserts
7
+ * live compositions against it; a durable adapter (`@byok-sdk/cloud-postgres`) is
8
+ * written against it.
9
+ *
10
+ * This module adds data and nothing else. It does not re-declare, re-shape, or
11
+ * re-interpret a single line of `ports.ts` — that file stays the authority for
12
+ * what the methods mean, and stayed byte-identical through S4A-a.
13
+ *
14
+ * Adding a port method means editing this table, which is the point: a port
15
+ * grows by contract, not by whichever composition needed something.
16
+ */
17
+ import type { CloudStoreName } from './ports';
18
+ export declare const CLOUD_PORT_METHODS: Readonly<Record<CloudStoreName, readonly string[]>>;
19
+ /** The interface each port name is declared as, for a source-side scan. */
20
+ export declare const CLOUD_PORT_INTERFACES: Readonly<Record<CloudStoreName, string>>;