@tinycloud/sdk-services 3.11.0-beta.2 → 3.12.0-beta.10
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.
- package/dist/{BaseService-Ma2MwJAd.d.ts → BaseService-BRDG69KR.d.ts} +1 -1
- package/dist/{BaseService-O0iyuFLk.d.cts → BaseService-CdBkfPGE.d.cts} +1 -1
- package/dist/{IHooksService-4QNODVy6.d.cts → IHooksService-C9p3stsB.d.cts} +1 -1
- package/dist/{IHooksService-CvTpJbQD.d.ts → IHooksService-D6eyZat1.d.ts} +1 -1
- package/dist/{ISQLService-Aly1CBsW.d.ts → ISQLService-B_4KcGA-.d.ts} +1 -1
- package/dist/{ISQLService-BOgWzHLT.d.cts → ISQLService-C6nNpcls.d.cts} +1 -1
- package/dist/context.cjs +4 -0
- package/dist/context.cjs.map +1 -1
- package/dist/context.d.cts +1 -1
- package/dist/context.d.ts +1 -1
- package/dist/context.js +4 -0
- package/dist/context.js.map +1 -1
- package/dist/encryption/index.cjs +276 -14
- package/dist/encryption/index.cjs.map +1 -1
- package/dist/encryption/index.d.cts +26 -28
- package/dist/encryption/index.d.ts +26 -28
- package/dist/encryption/index.js +268 -14
- package/dist/encryption/index.js.map +1 -1
- package/dist/encryption/named-keys.cjs +225 -14
- package/dist/encryption/named-keys.cjs.map +1 -1
- package/dist/encryption/named-keys.d.cts +7 -2
- package/dist/encryption/named-keys.d.ts +7 -2
- package/dist/encryption/named-keys.js +225 -14
- package/dist/encryption/named-keys.js.map +1 -1
- package/dist/encryption/resource.cjs.map +1 -1
- package/dist/encryption/resource.js.map +1 -1
- package/dist/feed/index.cjs +4 -0
- package/dist/feed/index.cjs.map +1 -1
- package/dist/feed/index.d.cts +3 -3
- package/dist/feed/index.d.ts +3 -3
- package/dist/feed/index.js +4 -0
- package/dist/feed/index.js.map +1 -1
- package/dist/index.cjs +509 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -12
- package/dist/index.d.ts +14 -12
- package/dist/index.js +499 -18
- package/dist/index.js.map +1 -1
- package/dist/internal/decrypt-transport-response-error.d.cts +1 -1
- package/dist/internal/decrypt-transport-response-error.d.ts +1 -1
- package/dist/kv/index.cjs +34 -0
- package/dist/kv/index.cjs.map +1 -1
- package/dist/kv/index.d.cts +4 -4
- package/dist/kv/index.d.ts +4 -4
- package/dist/kv/index.js +34 -0
- package/dist/kv/index.js.map +1 -1
- package/dist/kv/replication/index.cjs +39 -8
- package/dist/kv/replication/index.cjs.map +1 -1
- package/dist/kv/replication/index.d.cts +3 -3
- package/dist/kv/replication/index.d.ts +3 -3
- package/dist/kv/replication/index.js +38 -7
- package/dist/kv/replication/index.js.map +1 -1
- package/dist/{scope-DEgc9f-W.d.cts → scope-DtcbyRKY.d.cts} +1 -1
- package/dist/{scope-BZr0QwxX.d.ts → scope-J20PQP3u.d.ts} +1 -1
- package/dist/sql/index.cjs +36 -0
- package/dist/sql/index.cjs.map +1 -1
- package/dist/sql/index.d.cts +4 -4
- package/dist/sql/index.d.ts +4 -4
- package/dist/sql/index.js +36 -0
- package/dist/sql/index.js.map +1 -1
- package/dist/telemetry/requestLog.cjs +4 -0
- package/dist/telemetry/requestLog.cjs.map +1 -1
- package/dist/telemetry/requestLog.d.cts +1 -1
- package/dist/telemetry/requestLog.d.ts +1 -1
- package/dist/telemetry/requestLog.js +4 -0
- package/dist/telemetry/requestLog.js.map +1 -1
- package/dist/{types-D4XbIfFJ.d.cts → types-DfqNz9Nu.d.cts} +135 -6
- package/dist/{types-CSd0RYir.d.ts → types-DjAP8dve.d.cts} +4 -0
- package/dist/{types-CSd0RYir.d.cts → types-DjAP8dve.d.ts} +4 -0
- package/dist/{types-Bf91gup6.d.ts → types-Nvi_RckJ.d.ts} +135 -6
- package/package.json +4 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/telemetry/requestLog.ts","../../src/types.ts"],"sourcesContent":["/**\n * Developer request log core. Events are sanitized by `buildRequestEvent`, the\n * only constructor, which copies an allowlist of fields. Values, headers,\n * bodies, tokens, delegations, and error text have no field to land in.\n */\n\nimport { TelemetryEvents, type IServiceContext } from \"../types\";\n\nexport type RequestService =\n | \"kv\"\n | \"sql\"\n | \"duckdb\"\n | \"hooks\"\n | \"vault\"\n | \"space\"\n | \"sync\"\n | \"other\";\n\nexport interface RequestEvent {\n v: 1;\n /** Monotonic per RequestLog. */\n id: number;\n /** Request start, ms since epoch. */\n at: number;\n service: RequestService;\n /** `service/action`, e.g. `kv/get`; `other` when unknown. */\n op: string;\n /** `first/` of the key (`[redacted]` when unsafe or slashless); full key only for allowlisted first segments. */\n path: string | null;\n /** Tail label of the space id, never the id. */\n space: string | null;\n /** URL host[:port] only. */\n host: string | null;\n /** Body bytes for writes, response bytes for reads; null when unknown. */\n bytes: number | null;\n /** Whole logical request, including retry backoff; null when unknown. */\n durationMs: number | null;\n attempts: number;\n ok: boolean;\n code: string;\n source: \"network\" | \"replica\";\n}\n\n/** A sanitized event before the log assigns its id. */\nexport type RequestDraft = Omit<RequestEvent, \"id\">;\n\nexport interface RequestLogReader {\n /** Copy, oldest first. */\n snapshot(): readonly RequestEvent[];\n subscribe(cb: (event: RequestEvent) => void): () => void;\n onClear(cb: () => void): () => void;\n}\n\n/**\n * The fetch layer's only view of the developer request log. A context without a\n * sink never links the sanitizer or the invocation-meta store.\n */\nexport interface RequestSink {\n record(event: RequestDraft): void;\n /** Derive and record one event from one transport attempt's raw facts. */\n observe(observation: FetchObservation): void;\n /** Attach service, path, and action to headers built for an invocation. */\n remember(headers: object, meta: InvocationMeta): void;\n}\n\n/** Transport facts for one attempt, read by `observeFetch`. */\nexport interface FetchObservation {\n url: string;\n requestHeaders: unknown;\n requestBody: unknown;\n responseContentLength: string | null | undefined;\n ok: boolean;\n code: string;\n durationMs: number;\n attempts: number;\n at: number;\n}\n\n/** Raw facts about one logical request. Only `buildRequestEvent` reads them. */\nexport interface RawRequest {\n service?: string;\n op?: string;\n /** Unscrubbed key path. */\n path?: string | null;\n /** Full space id; only its tail label is kept. */\n spaceId?: string | null;\n url?: string | null;\n bytes?: number | null;\n durationMs: number | null;\n attempts?: number;\n ok: boolean;\n code?: string;\n source?: string;\n at: number;\n}\n\nconst RING_SIZE = 500;\nconst MAX_PATH = 256;\nconst REDACTED = \"[redacted]\";\n\nconst SERVICE_NAME = /^(kv|sql|duckdb|hooks|vault|space|sync)$/;\nconst OP_SHAPE = /^[a-z]+\\/[A-Za-z]+$/;\nconst SPACE_LABEL = /^[a-z0-9][a-z0-9_-]{0,31}$/;\nconst CODE_SHAPE = /^(ok|HTTP_[1-5]\\d\\d|[A-Z][A-Z0-9_]{1,63}|ERROR)$/;\nconst PATH_LABEL = /^[a-z0-9][a-z0-9_-]{0,31}$/;\n/** First segments whose full key may be recorded. Empty: only `first/` is ever kept. */\nconst PATH_ALLOWLIST: ReadonlySet<string> = new Set<string>();\n\nfunction scrubPath(raw: string | null | undefined): string | null {\n if (typeof raw !== \"string\") return null;\n const bare = raw.split(/[?#]/, 1)[0] ?? \"\";\n if (bare === \"\") return null;\n let decoded: string;\n try {\n decoded = decodeURIComponent(bare);\n } catch {\n return REDACTED;\n }\n const stripped = decoded.replace(/^\\/+/, \"\").replace(/\\/+/g, \"/\");\n if (stripped === \"\") return null;\n const slash = stripped.indexOf(\"/\");\n // Slashless: the whole value may be a bare secret name.\n if (slash === -1) return REDACTED;\n const first = stripped.slice(0, slash).toLowerCase();\n if (!PATH_LABEL.test(first)) return REDACTED;\n if (PATH_ALLOWLIST.has(first)) return stripped.length > MAX_PATH ? stripped.slice(0, MAX_PATH) : stripped;\n return `${first}/`;\n}\n\nfunction spaceLabel(spaceId: string | null | undefined): string | null {\n if (typeof spaceId !== \"string\" || spaceId === \"\") return null;\n // Only the name after the last colon is kept; an id without a colon has no such name.\n const colon = spaceId.lastIndexOf(\":\");\n if (colon === -1) return \"space\";\n const tail = spaceId.slice(colon + 1);\n return SPACE_LABEL.test(tail) ? tail : \"space\";\n}\n\nfunction hostOf(url: string | null | undefined): string | null {\n if (typeof url !== \"string\") return null;\n try {\n return new URL(url).host || null;\n } catch {\n return null;\n }\n}\n\n/** UTF-8 byte length of a request body without consuming it; null when unknown. */\nexport function byteLengthOf(body: unknown): number | null {\n if (typeof body === \"string\") return new TextEncoder().encode(body).byteLength;\n if (typeof Blob !== \"undefined\" && body instanceof Blob) return body.size;\n if (body instanceof ArrayBuffer) return body.byteLength;\n if (ArrayBuffer.isView(body)) return body.byteLength;\n return null;\n}\n\n/** Response Content-Length as bytes; null unless the header is plain digits (blank, signed, or exponent forms are unknown). */\nexport function declaredLengthOf(header: string | null | undefined): number | null {\n if (typeof header !== \"string\") return null;\n const value = header.trim();\n return /^\\d+$/.test(value) ? Number(value) : null;\n}\n\n/** What the signing path knows about a request; read back by the fetch layer. */\nexport interface InvocationMeta {\n service: string;\n path: string;\n action: string;\n spaceId?: string | null;\n}\n\nconst invocationMeta = new WeakMap<object, InvocationMeta>();\n\nexport function invocationMetaOf(headers: unknown): InvocationMeta | undefined {\n return typeof headers === \"object\" && headers !== null ? invocationMeta.get(headers) : undefined;\n}\n\nexport function buildRequestEvent(raw: RawRequest): RequestDraft {\n const service: RequestService =\n raw.service !== undefined && SERVICE_NAME.test(raw.service)\n ? (raw.service as RequestService)\n : \"other\";\n const space = spaceLabel(raw.spaceId);\n const op = typeof raw.op === \"string\" && OP_SHAPE.test(raw.op) ? raw.op : \"other\";\n const code =\n raw.code === undefined ? (raw.ok ? \"ok\" : \"ERROR\") : CODE_SHAPE.test(raw.code) ? raw.code : \"ERROR\";\n const bytes =\n typeof raw.bytes === \"number\" && Number.isFinite(raw.bytes) && raw.bytes >= 0\n ? Math.floor(raw.bytes)\n : null;\n return {\n v: 1,\n at: raw.at,\n service,\n op,\n path: scrubPath(raw.path),\n space,\n host: hostOf(raw.url),\n bytes,\n durationMs: typeof raw.durationMs === \"number\" && Number.isFinite(raw.durationMs) ? Math.max(0, Math.round(raw.durationMs)) : null,\n attempts:\n raw.attempts !== undefined && Number.isFinite(raw.attempts) && raw.attempts >= 1\n ? Math.floor(raw.attempts)\n : 1,\n ok: raw.ok === true,\n code,\n source: raw.source === \"replica\" ? \"replica\" : \"network\",\n };\n}\n\n/**\n * Derives one event from one transport attempt. Invocation metadata comes from the\n * request headers. Bytes are the request body length when a body was sent (null for\n * streams); with no request body, the declared response length.\n */\nexport function observeFetch(observation: FetchObservation, emit: (event: RequestDraft) => void): void {\n const meta = invocationMetaOf(observation.requestHeaders);\n emit(\n buildRequestEvent({\n service: meta?.service ?? \"other\",\n op: meta ? `${meta.service}/${meta.action.split(\"/\").pop() ?? meta.action}` : \"other\",\n path: meta?.path ?? null,\n spaceId: meta?.spaceId ?? null,\n url: observation.url,\n bytes: requestBytesOf(observation.requestBody, observation.responseContentLength),\n durationMs: observation.durationMs,\n attempts: observation.attempts,\n ok: observation.ok,\n code: observation.code,\n at: observation.at,\n }),\n );\n}\n\n/**\n * Request bytes: the body length when a body was sent (null when it cannot be measured\n * without consuming it); with no body, the declared response Content-Length.\n */\nexport function requestBytesOf(body: unknown, responseContentLength: string | null | undefined): number | null {\n if (body === undefined || body === null) return declaredLengthOf(responseContentLength);\n return byteLengthOf(body);\n}\n\n/**\n * Maps replica-served KV reads (`replication.read` with `source: \"replica\"`) to\n * request events. Network reads and replication writes are not mapped here:\n * their wire requests are recorded by the fetch layer.\n */\nexport function observeReplicaReads(context: IServiceContext, sink: RequestSink): () => void {\n return context.on(TelemetryEvents.REPLICATION_READ, (data: unknown) => {\n try {\n const read = (typeof data === \"object\" && data !== null ? data : {}) as Record<string, unknown>;\n if (read.source !== \"replica\") return;\n sink.record(\n buildRequestEvent({\n service: \"kv\",\n op: `kv/${String(read.op)}`,\n path: typeof read.key === \"string\" ? read.key : null,\n spaceId: typeof read.space === \"string\" ? read.space : null,\n url: null,\n bytes: null,\n durationMs: typeof read.latencyMs === \"number\" ? read.latencyMs : null,\n attempts: 1,\n ok: read.outcome !== \"error\",\n code: read.outcome === \"not_found\" ? \"KV_NOT_FOUND\" : typeof read.code === \"string\" ? read.code : undefined,\n source: \"replica\",\n at: Date.now(),\n }),\n );\n } catch {\n // Observability must not affect KV behavior.\n }\n });\n}\n\ntype Listener<T extends unknown[]> = (...args: T) => void;\n\n/** Fixed-size in-memory ring of sanitized events. Lost on reload by design (v1). */\nexport class RequestLog implements RequestSink, RequestLogReader {\n readonly #slots: (RequestEvent | undefined)[] = new Array(RING_SIZE);\n #head = 0;\n #count = 0;\n #nextId = 1;\n readonly #subscribers = new Set<Listener<[RequestEvent]>>();\n readonly #clearListeners = new Set<Listener<[]>>();\n\n record(draft: RequestDraft): void {\n const event: RequestEvent = Object.freeze({ ...draft, id: this.#nextId++ });\n this.#slots[this.#head] = event;\n this.#head = (this.#head + 1) % RING_SIZE;\n if (this.#count < RING_SIZE) this.#count++;\n for (const cb of this.#subscribers) {\n try {\n cb(event);\n } catch {\n // A failing subscriber must not break the request path or other subscribers.\n }\n }\n }\n\n /** Called only when a sink is set, so developer-off code never writes here. */\n remember(headers: object, meta: InvocationMeta): void {\n invocationMeta.set(headers, meta);\n }\n\n observe(observation: FetchObservation): void {\n observeFetch(observation, (event) => this.record(event));\n }\n\n snapshot(): RequestEvent[] {\n const start = this.#count < RING_SIZE ? 0 : this.#head;\n const out: RequestEvent[] = [];\n for (let i = 0; i < this.#count; i++) {\n out.push(this.#slots[(start + i) % RING_SIZE]!);\n }\n return out;\n }\n\n subscribe(cb: (event: RequestEvent) => void): () => void {\n this.#subscribers.add(cb);\n return () => {\n this.#subscribers.delete(cb);\n };\n }\n\n onClear(cb: () => void): () => void {\n this.#clearListeners.add(cb);\n return () => {\n this.#clearListeners.delete(cb);\n };\n }\n\n clear(): void {\n this.#slots.fill(undefined);\n this.#head = 0;\n this.#count = 0;\n for (const cb of this.#clearListeners) {\n try {\n cb();\n } catch {\n // Isolated like subscribers.\n }\n }\n }\n}\n\n/** The public view of a log: reads and subscriptions only. `record` and `clear` stay with the owner. */\nexport function readOnlyRequestLog(log: RequestLog): RequestLogReader {\n return Object.freeze({\n snapshot: () => log.snapshot(),\n subscribe: (cb: (event: RequestEvent) => void) => log.subscribe(cb),\n onClear: (cb: () => void) => log.onClear(cb),\n });\n}\n","/**\n * SDK Services - Core Types\n *\n * These types define the service architecture for TinyCloud SDK.\n * Services use dependency injection via IServiceContext for platform independence.\n */\n\n// =============================================================================\n// Result Type Pattern\n// =============================================================================\n\n/**\n * Result type for service operations.\n * Services return Result instead of throwing, making error handling explicit.\n *\n * @template T - The success data type\n * @template E - The error type (defaults to ServiceError)\n *\n * @example\n * ```typescript\n * const result = await kv.get('key');\n * if (result.ok) {\n * console.log(result.data);\n * } else {\n * console.error(result.error.code);\n * }\n * ```\n */\nexport type Result<T, E = ServiceError> =\n | { ok: true; data: T }\n | { ok: false; error: E };\n\n/**\n * Service error with structured information.\n */\nexport interface ServiceError {\n /** Error code for programmatic handling (e.g., 'KV_NOT_FOUND', 'AUTH_EXPIRED') */\n code: string;\n /** Human-readable error message */\n message: string;\n /** Service that produced the error (e.g., 'kv', 'sql') */\n service: string;\n /** Original error if this wraps another error */\n cause?: Error;\n /** Additional metadata about the error */\n meta?: Record<string, unknown>;\n}\n\n/**\n * A node-supplied permission hint after strict SDK validation. This is\n * deliberately narrower than a general capability: no wildcards, caveats,\n * descriptions, or transport metadata can cross the service boundary.\n */\nexport interface PermissionHint {\n readonly service: \"tinycloud.kv\" | \"tinycloud.encryption\";\n readonly space?: string;\n readonly path: string;\n readonly actions: readonly string[];\n}\n\n/**\n * Storage quota information returned with quota-related errors.\n * @deprecated Read `usedBytes`, `limitBytes` and `account` from the error's\n * `meta`, or use `StorageFullEvent` from `tc.on(\"storage.full\", handler)`.\n */\nexport interface StorageQuotaInfo {\n usedBytes: number;\n limitBytes: number;\n service: string;\n}\n\n/**\n * Standard error codes used across services.\n */\nexport const ErrorCodes = {\n // Common errors\n NOT_FOUND: \"NOT_FOUND\",\n AUTH_EXPIRED: \"AUTH_EXPIRED\",\n AUTH_REQUIRED: \"AUTH_REQUIRED\",\n AUTH_UNAUTHORIZED: \"AUTH_UNAUTHORIZED\",\n /** HTTP 401: the delegation the invocation cites was revoked. */\n AUTH_DELEGATION_REVOKED: \"AUTH_DELEGATION_REVOKED\",\n /** HTTP 401: a delegation the cited one rests on was revoked. */\n AUTH_DELEGATION_ANCESTOR_REVOKED: \"AUTH_DELEGATION_ANCESTOR_REVOKED\",\n NETWORK_ERROR: \"NETWORK_ERROR\",\n TIMEOUT: \"TIMEOUT\",\n ABORTED: \"ABORTED\",\n INVALID_INPUT: \"INVALID_INPUT\",\n PERMISSION_DENIED: \"PERMISSION_DENIED\",\n\n // Node retry signals (tinycloud-node #347). `meta.retryable` says whether a\n // retry is safe; `meta.retryAfterMs` carries the node's Retry-After.\n /** HTTP 503 `sql_busy:<stage>` / `unavailable_retryable`/`read_audit_busy`: nothing was applied; retry after `meta.retryAfterMs`. */\n NODE_BUSY: \"NODE_BUSY\",\n /** HTTP 503 `request_timeout`: the node's deadline passed before any write began; nothing was applied. */\n REQUEST_TIMEOUT: \"REQUEST_TIMEOUT\",\n /** HTTP 500 `outcome_unknown`: a write may have been applied; read back before retrying a non-idempotent operation. */\n OUTCOME_UNKNOWN: \"OUTCOME_UNKNOWN\",\n\n // KV-specific errors\n KV_NOT_FOUND: \"KV_NOT_FOUND\",\n KV_WRITE_FAILED: \"KV_WRITE_FAILED\",\n KV_PRECONDITION_FAILED: \"KV_PRECONDITION_FAILED\",\n KV_CONFLICT: \"KV_CONFLICT\",\n KV_RESPONSE_TOO_LARGE: \"KV_RESPONSE_TOO_LARGE\",\n /** HTTP 410 from the change feed: discard sync state and restart without a cursor; `meta.reason` says why. */\n KV_SYNC_RESET_REQUIRED: \"KV_SYNC_RESET_REQUIRED\",\n /** HTTP 403 from the change feed: the retention grant was refused; `meta.reason` says why. */\n KV_RETENTION_GRANT_REFUSED: \"KV_RETENTION_GRANT_REFUSED\",\n\n // SQL-specific errors\n SQL_ERROR: \"SQL_ERROR\",\n SQL_PERMISSION_DENIED: \"SQL_PERMISSION_DENIED\",\n SQL_DATABASE_NOT_FOUND: \"SQL_DATABASE_NOT_FOUND\",\n SQL_RESPONSE_TOO_LARGE: \"SQL_RESPONSE_TOO_LARGE\",\n SQL_QUOTA_EXCEEDED: \"SQL_QUOTA_EXCEEDED\",\n SQL_INVALID_STATEMENT: \"SQL_INVALID_STATEMENT\",\n SQL_SCHEMA_ERROR: \"SQL_SCHEMA_ERROR\",\n SQL_READONLY_VIOLATION: \"SQL_READONLY_VIOLATION\",\n\n // Storage quota errors\n STORAGE_QUOTA_EXCEEDED: \"STORAGE_QUOTA_EXCEEDED\",\n STORAGE_LIMIT_REACHED: \"STORAGE_LIMIT_REACHED\",\n /** The node answered the usage read without usage numbers (it predates the read). */\n STORAGE_STATUS_UNAVAILABLE: \"STORAGE_STATUS_UNAVAILABLE\",\n\n // DuckDB-specific errors\n DUCKDB_ERROR: \"DUCKDB_ERROR\",\n DUCKDB_PERMISSION_DENIED: \"DUCKDB_PERMISSION_DENIED\",\n DUCKDB_DATABASE_NOT_FOUND: \"DUCKDB_DATABASE_NOT_FOUND\",\n DUCKDB_RESPONSE_TOO_LARGE: \"DUCKDB_RESPONSE_TOO_LARGE\",\n DUCKDB_QUOTA_EXCEEDED: \"DUCKDB_QUOTA_EXCEEDED\",\n DUCKDB_INVALID_STATEMENT: \"DUCKDB_INVALID_STATEMENT\",\n DUCKDB_SCHEMA_ERROR: \"DUCKDB_SCHEMA_ERROR\",\n DUCKDB_READONLY_VIOLATION: \"DUCKDB_READONLY_VIOLATION\",\n} as const;\n\nexport type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];\n\n// =============================================================================\n// Service Session\n// =============================================================================\n\n/**\n * Session data required for authenticated service operations.\n * Both TinyCloudSession and web-sdk Session can be cast to this interface.\n */\nexport interface ServiceSession {\n /** The delegation header containing the UCAN */\n delegationHeader: { Authorization: string };\n /** The delegation CID */\n delegationCid: string;\n /** The space ID for this session */\n spaceId: string;\n /** The verification method DID */\n verificationMethod: string;\n /** The session key JWK (required for invoke) */\n jwk: object;\n}\n\n// =============================================================================\n// Platform Dependencies (Injected)\n// =============================================================================\n\n/**\n * Headers type - compatible with both browser and Node.js.\n */\nexport type ServiceHeaders = Record<string, string> | [string, string][];\n\n/**\n * A single fact object to include in the UCAN invocation.\n * Facts are key-value objects that the server reads from the UCAN facts field.\n */\nexport interface InvocationFact {\n [key: string]: unknown;\n}\n\n/**\n * Facts to include in the UCAN invocation.\n * This is an array of fact objects per the UCAN spec.\n * Used to pass additional parameters that the server reads from the UCAN facts field.\n */\nexport type InvocationFacts = InvocationFact[];\n\n/**\n * Invoke function signature - platform-specific implementation injected via DI.\n * Both node-sdk-wasm and web-sdk-wasm export this with identical signature.\n *\n * @param session - The service session with delegation data\n * @param service - Service name (e.g., \"kv\")\n * @param path - Resource path or key\n * @param action - Action to perform (e.g., \"tinycloud.kv/get\")\n * @param facts - Optional facts to include in the UCAN (e.g., for capabilities/read params)\n * @returns Headers to include in the request\n */\nexport type InvokeFunction = (\n session: ServiceSession,\n service: string,\n path: string,\n action: string,\n facts?: InvocationFacts,\n) => ServiceHeaders;\n\n/**\n * Multi-resource invocation entry.\n */\nexport interface InvokeAnyEntry {\n /**\n * Legacy space-scoped resource. Optional when `resource` is provided.\n */\n spaceId?: string;\n service: string;\n path: string;\n action: string;\n /** Optional raw resource URI. When set, WASM signs this URI directly. */\n resource?: string;\n /** Capability caveats that must remain on this invocation attenuation. */\n caveats?: Record<string, unknown>[];\n}\n\n/**\n * Invoke function for minting a single authorization header that covers\n * multiple capabilities across one effective invoker.\n */\nexport type InvokeAnyFunction = (\n session: ServiceSession,\n entries: InvokeAnyEntry[],\n facts?: InvocationFacts,\n) => ServiceHeaders;\n\n/**\n * Fetch request options - compatible with standard fetch API.\n */\nexport interface FetchRequestInit {\n method?: string;\n headers?: ServiceHeaders;\n body?: Blob | FormData | string;\n signal?: AbortSignal;\n redirect?: \"error\";\n}\n\n/**\n * Fetch response interface - compatible with standard Response.\n */\nexport interface FetchResponse {\n ok: boolean;\n status: number;\n statusText: string;\n body?: unknown;\n headers: {\n get(name: string): string | null;\n };\n json(): Promise<unknown>;\n text(): Promise<string>;\n arrayBuffer(): Promise<ArrayBuffer>;\n blob(): Promise<Blob>;\n /** Present on standard Responses; the retry reads a clone so the caller's body stays intact. */\n clone?(): FetchResponse;\n /** Set by `ServiceContext.invokeWithRetry` on the response it returns: how many requests it took. */\n retry?: RetryStats;\n}\n\n/** What automatic retry cost one request, so latency can be split from backoff. */\nexport interface RetryStats {\n /** Requests sent, including the first (1 means no retry). */\n attempts: number;\n /** Total milliseconds spent waiting between attempts. */\n backoffMs: number;\n}\n\n/**\n * Fetch function signature - allows for custom fetch implementations.\n * Compatible with both browser fetch and Node.js fetch.\n */\nexport type FetchFunction = (\n url: string,\n init?: FetchRequestInit,\n) => Promise<FetchResponse>;\n\n// =============================================================================\n// Retry Policy\n// =============================================================================\n\n/**\n * Automatic retry of node responses that guarantee nothing was applied\n * (`NODE_BUSY`, `REQUEST_TIMEOUT`). Applied once, in the service context's\n * `invokeWithRetry`, which re-mints the signed invocation per attempt. Backoff\n * is full-jitter: each delay is uniform in `[0, step]`, where\n * `step` is the strategy's delay capped at `maxDelayMs`. A node `Retry-After`\n * replaces the delay. Opt out with `{ maxAttempts: 1 }`.\n */\nexport interface RetryPolicy {\n /** Maximum number of attempts (including initial) */\n maxAttempts: number;\n /** Backoff strategy between retries */\n backoff: \"none\" | \"linear\" | \"exponential\";\n /** Base delay in milliseconds for backoff calculation */\n baseDelayMs: number;\n /** Maximum delay in milliseconds between retries */\n maxDelayMs: number;\n /** Node error codes that trigger a retry. `OUTCOME_UNKNOWN` is never retried. */\n retryableErrors: string[];\n /** Total time budget for all attempts and waits, in milliseconds. Unset: no cap. */\n maxTotalMs?: number;\n}\n\n/**\n * Default retry policy.\n */\nexport const defaultRetryPolicy: RetryPolicy = {\n maxAttempts: 4,\n backoff: \"exponential\",\n baseDelayMs: 250,\n maxDelayMs: 4000,\n retryableErrors: [ErrorCodes.NODE_BUSY, ErrorCodes.REQUEST_TIMEOUT],\n maxTotalMs: 12000,\n};\n\n// =============================================================================\n// Service Context\n// =============================================================================\n\n/**\n * Event handler function type.\n */\nexport type EventHandler = (data: unknown) => void;\n\n/**\n * SDK telemetry event handler.\n */\nexport type TelemetryEventHandler = (event: string, data: unknown) => void;\n\n/**\n * Default-off telemetry configuration.\n */\nexport type TelemetryConfig =\n | boolean\n | {\n enabled?: boolean;\n onEvent?: TelemetryEventHandler;\n };\n\n/**\n * Service interface - base contract for all services.\n */\nexport interface IService {\n /** Initialize service with context */\n initialize(context: IServiceContext): void;\n\n /** Called when session changes (sign-in, sign-out, refresh) */\n onSessionChange(session: ServiceSession | null): void;\n\n /** Called when SDK signs out - should abort pending operations */\n onSignOut(): void;\n\n /** Service-specific configuration */\n readonly config: Record<string, unknown>;\n}\n\n/**\n * Context provided to services for accessing platform dependencies.\n * The SDK creates this context and passes it to services during initialization.\n */\nexport interface IServiceContext {\n // Session management\n /** Current active session, or null if not authenticated */\n readonly session: ServiceSession | null;\n /** Whether there is an active authenticated session */\n readonly isAuthenticated: boolean;\n\n // Platform dependencies (injected by SDK)\n /** Platform-specific invoke function from WASM binding */\n readonly invoke: InvokeFunction;\n /** Optional multi-resource invoke function */\n readonly invokeAny?: InvokeAnyFunction;\n /** Fetch function (defaults to globalThis.fetch) */\n readonly fetch: FetchFunction;\n /** Send a signed invocation, retrying a busy node; `build` re-signs per attempt. Absent: sends once. */\n invokeWithRetry?(\n url: string,\n build: () => FetchRequestInit | Promise<FetchRequestInit>,\n ): Promise<FetchResponse>;\n /** Available TinyCloud host URLs */\n readonly hosts: string[];\n\n // Cross-service access\n /** Get another registered service by name */\n getService<T extends IService>(name: string): T | undefined;\n\n // Telemetry/Events\n /** Emit a telemetry event */\n emit(event: string, data: unknown): void;\n /** Subscribe to events */\n on(event: string, handler: EventHandler): () => void;\n\n // Lifecycle\n /** Abort signal that fires when SDK signs out. */\n readonly abortSignal: AbortSignal;\n /** Additional abort signal for a scoped operation, when active. */\n readonly operationAbortSignal?: AbortSignal;\n /** Install an additional signal for the duration of a scoped operation. */\n setOperationAbortSignal?(signal: AbortSignal | undefined): void;\n // Retry policy\n /** Retry policy for failed requests */\n readonly retryPolicy: RetryPolicy;\n}\n\n// =============================================================================\n// Telemetry Events\n// =============================================================================\n\n/**\n * Event emitted before a service request.\n */\nexport interface ServiceRequestEvent {\n service: string;\n action: string;\n span?: string;\n key?: string;\n timestamp: number;\n}\n\n/**\n * Event emitted after a service response.\n */\nexport interface ServiceResponseEvent {\n service: string;\n action: string;\n span?: string;\n ok: boolean;\n duration: number;\n durationMs?: number;\n status?: number;\n}\n\n/**\n * Event emitted on service error.\n */\nexport interface ServiceErrorEvent {\n service: string;\n span?: string;\n error: ServiceError;\n}\n\n/**\n * Event emitted on retry attempt.\n */\nexport interface ServiceRetryEvent {\n service: string;\n attempt: number;\n maxAttempts: number;\n error: ServiceError;\n}\n\n/**\n * Generic named span event for aggregating operation timings.\n */\nexport interface TelemetrySpanEvent {\n span: string;\n ok: boolean;\n durationMs: number;\n service?: string;\n action?: string;\n status?: number;\n error?: ServiceError;\n}\n\n/**\n * Telemetry event names.\n */\nexport const TelemetryEvents = {\n SPAN: \"telemetry.span\",\n SERVICE_REQUEST: \"service.request\",\n SERVICE_RESPONSE: \"service.response\",\n SERVICE_ERROR: \"service.error\",\n SERVICE_RETRY: \"service.retry\",\n SESSION_CHANGED: \"session.changed\",\n SESSION_EXPIRED: \"session.expired\",\n REPLICATION_READ: \"replication.read\",\n REPLICATION_WRITE: \"replication.write\",\n REPLICATION_SYNC: \"replication.sync\",\n REPLICATION_STATE: \"replication.state\",\n REPLICATION_DIVERGENCE: \"replication.divergence\",\n REPLICATION_GRANT: \"replication.grant\",\n REPLICATION_LIFECYCLE: \"replication.lifecycle\",\n /** Emitted once, on the first write rejected because storage is full. */\n STORAGE_FULL: \"storage.full\",\n} as const;\n\n// =============================================================================\n// Helper Functions\n// =============================================================================\n\n/**\n * Create a success result.\n */\nexport function ok<T>(data: T): Result<T> {\n return { ok: true, data };\n}\n\n/**\n * Create an error result.\n */\nexport function err<E = ServiceError>(error: E): Result<never, E> {\n return { ok: false, error };\n}\n\n/**\n * Create a ServiceError.\n */\nexport function serviceError(\n code: string,\n message: string,\n service: string,\n options?: { cause?: Error; meta?: Record<string, unknown> },\n): ServiceError {\n return {\n code,\n message,\n service,\n cause: options?.cause,\n meta: options?.meta,\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/telemetry/requestLog.ts","../../src/types.ts"],"sourcesContent":["/**\n * Developer request log core. Events are sanitized by `buildRequestEvent`, the\n * only constructor, which copies an allowlist of fields. Values, headers,\n * bodies, tokens, delegations, and error text have no field to land in.\n */\n\nimport { TelemetryEvents, type IServiceContext } from \"../types\";\n\nexport type RequestService =\n | \"kv\"\n | \"sql\"\n | \"duckdb\"\n | \"hooks\"\n | \"vault\"\n | \"space\"\n | \"sync\"\n | \"other\";\n\nexport interface RequestEvent {\n v: 1;\n /** Monotonic per RequestLog. */\n id: number;\n /** Request start, ms since epoch. */\n at: number;\n service: RequestService;\n /** `service/action`, e.g. `kv/get`; `other` when unknown. */\n op: string;\n /** `first/` of the key (`[redacted]` when unsafe or slashless); full key only for allowlisted first segments. */\n path: string | null;\n /** Tail label of the space id, never the id. */\n space: string | null;\n /** URL host[:port] only. */\n host: string | null;\n /** Body bytes for writes, response bytes for reads; null when unknown. */\n bytes: number | null;\n /** Whole logical request, including retry backoff; null when unknown. */\n durationMs: number | null;\n attempts: number;\n ok: boolean;\n code: string;\n source: \"network\" | \"replica\";\n}\n\n/** A sanitized event before the log assigns its id. */\nexport type RequestDraft = Omit<RequestEvent, \"id\">;\n\nexport interface RequestLogReader {\n /** Copy, oldest first. */\n snapshot(): readonly RequestEvent[];\n subscribe(cb: (event: RequestEvent) => void): () => void;\n onClear(cb: () => void): () => void;\n}\n\n/**\n * The fetch layer's only view of the developer request log. A context without a\n * sink never links the sanitizer or the invocation-meta store.\n */\nexport interface RequestSink {\n record(event: RequestDraft): void;\n /** Derive and record one event from one transport attempt's raw facts. */\n observe(observation: FetchObservation): void;\n /** Attach service, path, and action to headers built for an invocation. */\n remember(headers: object, meta: InvocationMeta): void;\n}\n\n/** Transport facts for one attempt, read by `observeFetch`. */\nexport interface FetchObservation {\n url: string;\n requestHeaders: unknown;\n requestBody: unknown;\n responseContentLength: string | null | undefined;\n ok: boolean;\n code: string;\n durationMs: number;\n attempts: number;\n at: number;\n}\n\n/** Raw facts about one logical request. Only `buildRequestEvent` reads them. */\nexport interface RawRequest {\n service?: string;\n op?: string;\n /** Unscrubbed key path. */\n path?: string | null;\n /** Full space id; only its tail label is kept. */\n spaceId?: string | null;\n url?: string | null;\n bytes?: number | null;\n durationMs: number | null;\n attempts?: number;\n ok: boolean;\n code?: string;\n source?: string;\n at: number;\n}\n\nconst RING_SIZE = 500;\nconst MAX_PATH = 256;\nconst REDACTED = \"[redacted]\";\n\nconst SERVICE_NAME = /^(kv|sql|duckdb|hooks|vault|space|sync)$/;\nconst OP_SHAPE = /^[a-z]+\\/[A-Za-z]+$/;\nconst SPACE_LABEL = /^[a-z0-9][a-z0-9_-]{0,31}$/;\nconst CODE_SHAPE = /^(ok|HTTP_[1-5]\\d\\d|[A-Z][A-Z0-9_]{1,63}|ERROR)$/;\nconst PATH_LABEL = /^[a-z0-9][a-z0-9_-]{0,31}$/;\n/** First segments whose full key may be recorded. Empty: only `first/` is ever kept. */\nconst PATH_ALLOWLIST: ReadonlySet<string> = new Set<string>();\n\nfunction scrubPath(raw: string | null | undefined): string | null {\n if (typeof raw !== \"string\") return null;\n const bare = raw.split(/[?#]/, 1)[0] ?? \"\";\n if (bare === \"\") return null;\n let decoded: string;\n try {\n decoded = decodeURIComponent(bare);\n } catch {\n return REDACTED;\n }\n const stripped = decoded.replace(/^\\/+/, \"\").replace(/\\/+/g, \"/\");\n if (stripped === \"\") return null;\n const slash = stripped.indexOf(\"/\");\n // Slashless: the whole value may be a bare secret name.\n if (slash === -1) return REDACTED;\n const first = stripped.slice(0, slash).toLowerCase();\n if (!PATH_LABEL.test(first)) return REDACTED;\n if (PATH_ALLOWLIST.has(first)) return stripped.length > MAX_PATH ? stripped.slice(0, MAX_PATH) : stripped;\n return `${first}/`;\n}\n\nfunction spaceLabel(spaceId: string | null | undefined): string | null {\n if (typeof spaceId !== \"string\" || spaceId === \"\") return null;\n // Only the name after the last colon is kept; an id without a colon has no such name.\n const colon = spaceId.lastIndexOf(\":\");\n if (colon === -1) return \"space\";\n const tail = spaceId.slice(colon + 1);\n return SPACE_LABEL.test(tail) ? tail : \"space\";\n}\n\nfunction hostOf(url: string | null | undefined): string | null {\n if (typeof url !== \"string\") return null;\n try {\n return new URL(url).host || null;\n } catch {\n return null;\n }\n}\n\n/** UTF-8 byte length of a request body without consuming it; null when unknown. */\nexport function byteLengthOf(body: unknown): number | null {\n if (typeof body === \"string\") return new TextEncoder().encode(body).byteLength;\n if (typeof Blob !== \"undefined\" && body instanceof Blob) return body.size;\n if (body instanceof ArrayBuffer) return body.byteLength;\n if (ArrayBuffer.isView(body)) return body.byteLength;\n return null;\n}\n\n/** Response Content-Length as bytes; null unless the header is plain digits (blank, signed, or exponent forms are unknown). */\nexport function declaredLengthOf(header: string | null | undefined): number | null {\n if (typeof header !== \"string\") return null;\n const value = header.trim();\n return /^\\d+$/.test(value) ? Number(value) : null;\n}\n\n/** What the signing path knows about a request; read back by the fetch layer. */\nexport interface InvocationMeta {\n service: string;\n path: string;\n action: string;\n spaceId?: string | null;\n}\n\nconst invocationMeta = new WeakMap<object, InvocationMeta>();\n\nexport function invocationMetaOf(headers: unknown): InvocationMeta | undefined {\n return typeof headers === \"object\" && headers !== null ? invocationMeta.get(headers) : undefined;\n}\n\nexport function buildRequestEvent(raw: RawRequest): RequestDraft {\n const service: RequestService =\n raw.service !== undefined && SERVICE_NAME.test(raw.service)\n ? (raw.service as RequestService)\n : \"other\";\n const space = spaceLabel(raw.spaceId);\n const op = typeof raw.op === \"string\" && OP_SHAPE.test(raw.op) ? raw.op : \"other\";\n const code =\n raw.code === undefined ? (raw.ok ? \"ok\" : \"ERROR\") : CODE_SHAPE.test(raw.code) ? raw.code : \"ERROR\";\n const bytes =\n typeof raw.bytes === \"number\" && Number.isFinite(raw.bytes) && raw.bytes >= 0\n ? Math.floor(raw.bytes)\n : null;\n return {\n v: 1,\n at: raw.at,\n service,\n op,\n path: scrubPath(raw.path),\n space,\n host: hostOf(raw.url),\n bytes,\n durationMs: typeof raw.durationMs === \"number\" && Number.isFinite(raw.durationMs) ? Math.max(0, Math.round(raw.durationMs)) : null,\n attempts:\n raw.attempts !== undefined && Number.isFinite(raw.attempts) && raw.attempts >= 1\n ? Math.floor(raw.attempts)\n : 1,\n ok: raw.ok === true,\n code,\n source: raw.source === \"replica\" ? \"replica\" : \"network\",\n };\n}\n\n/**\n * Derives one event from one transport attempt. Invocation metadata comes from the\n * request headers. Bytes are the request body length when a body was sent (null for\n * streams); with no request body, the declared response length.\n */\nexport function observeFetch(observation: FetchObservation, emit: (event: RequestDraft) => void): void {\n const meta = invocationMetaOf(observation.requestHeaders);\n emit(\n buildRequestEvent({\n service: meta?.service ?? \"other\",\n op: meta ? `${meta.service}/${meta.action.split(\"/\").pop() ?? meta.action}` : \"other\",\n path: meta?.path ?? null,\n spaceId: meta?.spaceId ?? null,\n url: observation.url,\n bytes: requestBytesOf(observation.requestBody, observation.responseContentLength),\n durationMs: observation.durationMs,\n attempts: observation.attempts,\n ok: observation.ok,\n code: observation.code,\n at: observation.at,\n }),\n );\n}\n\n/**\n * Request bytes: the body length when a body was sent (null when it cannot be measured\n * without consuming it); with no body, the declared response Content-Length.\n */\nexport function requestBytesOf(body: unknown, responseContentLength: string | null | undefined): number | null {\n if (body === undefined || body === null) return declaredLengthOf(responseContentLength);\n return byteLengthOf(body);\n}\n\n/**\n * Maps replica-served KV reads (`replication.read` with `source: \"replica\"`) to\n * request events. Network reads and replication writes are not mapped here:\n * their wire requests are recorded by the fetch layer.\n */\nexport function observeReplicaReads(context: IServiceContext, sink: RequestSink): () => void {\n return context.on(TelemetryEvents.REPLICATION_READ, (data: unknown) => {\n try {\n const read = (typeof data === \"object\" && data !== null ? data : {}) as Record<string, unknown>;\n if (read.source !== \"replica\") return;\n sink.record(\n buildRequestEvent({\n service: \"kv\",\n op: `kv/${String(read.op)}`,\n path: typeof read.key === \"string\" ? read.key : null,\n spaceId: typeof read.space === \"string\" ? read.space : null,\n url: null,\n bytes: null,\n durationMs: typeof read.latencyMs === \"number\" ? read.latencyMs : null,\n attempts: 1,\n ok: read.outcome !== \"error\",\n code: read.outcome === \"not_found\" ? \"KV_NOT_FOUND\" : typeof read.code === \"string\" ? read.code : undefined,\n source: \"replica\",\n at: Date.now(),\n }),\n );\n } catch {\n // Observability must not affect KV behavior.\n }\n });\n}\n\ntype Listener<T extends unknown[]> = (...args: T) => void;\n\n/** Fixed-size in-memory ring of sanitized events. Lost on reload by design (v1). */\nexport class RequestLog implements RequestSink, RequestLogReader {\n readonly #slots: (RequestEvent | undefined)[] = new Array(RING_SIZE);\n #head = 0;\n #count = 0;\n #nextId = 1;\n readonly #subscribers = new Set<Listener<[RequestEvent]>>();\n readonly #clearListeners = new Set<Listener<[]>>();\n\n record(draft: RequestDraft): void {\n const event: RequestEvent = Object.freeze({ ...draft, id: this.#nextId++ });\n this.#slots[this.#head] = event;\n this.#head = (this.#head + 1) % RING_SIZE;\n if (this.#count < RING_SIZE) this.#count++;\n for (const cb of this.#subscribers) {\n try {\n cb(event);\n } catch {\n // A failing subscriber must not break the request path or other subscribers.\n }\n }\n }\n\n /** Called only when a sink is set, so developer-off code never writes here. */\n remember(headers: object, meta: InvocationMeta): void {\n invocationMeta.set(headers, meta);\n }\n\n observe(observation: FetchObservation): void {\n observeFetch(observation, (event) => this.record(event));\n }\n\n snapshot(): RequestEvent[] {\n const start = this.#count < RING_SIZE ? 0 : this.#head;\n const out: RequestEvent[] = [];\n for (let i = 0; i < this.#count; i++) {\n out.push(this.#slots[(start + i) % RING_SIZE]!);\n }\n return out;\n }\n\n subscribe(cb: (event: RequestEvent) => void): () => void {\n this.#subscribers.add(cb);\n return () => {\n this.#subscribers.delete(cb);\n };\n }\n\n onClear(cb: () => void): () => void {\n this.#clearListeners.add(cb);\n return () => {\n this.#clearListeners.delete(cb);\n };\n }\n\n clear(): void {\n this.#slots.fill(undefined);\n this.#head = 0;\n this.#count = 0;\n for (const cb of this.#clearListeners) {\n try {\n cb();\n } catch {\n // Isolated like subscribers.\n }\n }\n }\n}\n\n/** The public view of a log: reads and subscriptions only. `record` and `clear` stay with the owner. */\nexport function readOnlyRequestLog(log: RequestLog): RequestLogReader {\n return Object.freeze({\n snapshot: () => log.snapshot(),\n subscribe: (cb: (event: RequestEvent) => void) => log.subscribe(cb),\n onClear: (cb: () => void) => log.onClear(cb),\n });\n}\n","/**\n * SDK Services - Core Types\n *\n * These types define the service architecture for TinyCloud SDK.\n * Services use dependency injection via IServiceContext for platform independence.\n */\n\n// =============================================================================\n// Result Type Pattern\n// =============================================================================\n\n/**\n * Result type for service operations.\n * Services return Result instead of throwing, making error handling explicit.\n *\n * @template T - The success data type\n * @template E - The error type (defaults to ServiceError)\n *\n * @example\n * ```typescript\n * const result = await kv.get('key');\n * if (result.ok) {\n * console.log(result.data);\n * } else {\n * console.error(result.error.code);\n * }\n * ```\n */\nexport type Result<T, E = ServiceError> =\n | { ok: true; data: T }\n | { ok: false; error: E };\n\n/**\n * Service error with structured information.\n */\nexport interface ServiceError {\n /** Error code for programmatic handling (e.g., 'KV_NOT_FOUND', 'AUTH_EXPIRED') */\n code: string;\n /** Human-readable error message */\n message: string;\n /** Service that produced the error (e.g., 'kv', 'sql') */\n service: string;\n /** Original error if this wraps another error */\n cause?: Error;\n /** Additional metadata about the error */\n meta?: Record<string, unknown>;\n}\n\n/**\n * A node-supplied permission hint after strict SDK validation. This is\n * deliberately narrower than a general capability: no wildcards, caveats,\n * descriptions, or transport metadata can cross the service boundary.\n */\nexport interface PermissionHint {\n readonly service: \"tinycloud.kv\" | \"tinycloud.encryption\";\n readonly space?: string;\n readonly path: string;\n readonly actions: readonly string[];\n}\n\n/**\n * Storage quota information returned with quota-related errors.\n * @deprecated Read `usedBytes`, `limitBytes` and `account` from the error's\n * `meta`, or use `StorageFullEvent` from `tc.on(\"storage.full\", handler)`.\n */\nexport interface StorageQuotaInfo {\n usedBytes: number;\n limitBytes: number;\n service: string;\n}\n\n/**\n * Standard error codes used across services.\n */\nexport const ErrorCodes = {\n // Common errors\n NOT_FOUND: \"NOT_FOUND\",\n AUTH_EXPIRED: \"AUTH_EXPIRED\",\n AUTH_REQUIRED: \"AUTH_REQUIRED\",\n AUTH_UNAUTHORIZED: \"AUTH_UNAUTHORIZED\",\n /** HTTP 401: the delegation the invocation cites was revoked. */\n AUTH_DELEGATION_REVOKED: \"AUTH_DELEGATION_REVOKED\",\n /** HTTP 401: a delegation the cited one rests on was revoked. */\n AUTH_DELEGATION_ANCESTOR_REVOKED: \"AUTH_DELEGATION_ANCESTOR_REVOKED\",\n NETWORK_ERROR: \"NETWORK_ERROR\",\n NODE_NOT_RESPONDING: \"NODE_NOT_RESPONDING\",\n NODE_IDENTITY_MISMATCH: \"NODE_IDENTITY_MISMATCH\",\n PARTIAL_NODE_WRITE: \"PARTIAL_NODE_WRITE\",\n RESOURCE_NOT_ROUTED: \"RESOURCE_NOT_ROUTED\",\n TIMEOUT: \"TIMEOUT\",\n ABORTED: \"ABORTED\",\n INVALID_INPUT: \"INVALID_INPUT\",\n PERMISSION_DENIED: \"PERMISSION_DENIED\",\n\n // Node retry signals (tinycloud-node #347). `meta.retryable` says whether a\n // retry is safe; `meta.retryAfterMs` carries the node's Retry-After.\n /** HTTP 503 `sql_busy:<stage>` / `unavailable_retryable`/`read_audit_busy`: nothing was applied; retry after `meta.retryAfterMs`. */\n NODE_BUSY: \"NODE_BUSY\",\n /** HTTP 503 `request_timeout`: the node's deadline passed before any write began; nothing was applied. */\n REQUEST_TIMEOUT: \"REQUEST_TIMEOUT\",\n /** HTTP 500 `outcome_unknown`: a write may have been applied; read back before retrying a non-idempotent operation. */\n OUTCOME_UNKNOWN: \"OUTCOME_UNKNOWN\",\n\n // KV-specific errors\n KV_NOT_FOUND: \"KV_NOT_FOUND\",\n KV_WRITE_FAILED: \"KV_WRITE_FAILED\",\n KV_PRECONDITION_FAILED: \"KV_PRECONDITION_FAILED\",\n KV_CONFLICT: \"KV_CONFLICT\",\n KV_RESPONSE_TOO_LARGE: \"KV_RESPONSE_TOO_LARGE\",\n /** HTTP 410 from the change feed: discard sync state and restart without a cursor; `meta.reason` says why. */\n KV_SYNC_RESET_REQUIRED: \"KV_SYNC_RESET_REQUIRED\",\n /** HTTP 403 from the change feed: the retention grant was refused; `meta.reason` says why. */\n KV_RETENTION_GRANT_REFUSED: \"KV_RETENTION_GRANT_REFUSED\",\n\n // SQL-specific errors\n SQL_ERROR: \"SQL_ERROR\",\n SQL_PERMISSION_DENIED: \"SQL_PERMISSION_DENIED\",\n SQL_DATABASE_NOT_FOUND: \"SQL_DATABASE_NOT_FOUND\",\n SQL_RESPONSE_TOO_LARGE: \"SQL_RESPONSE_TOO_LARGE\",\n SQL_QUOTA_EXCEEDED: \"SQL_QUOTA_EXCEEDED\",\n SQL_INVALID_STATEMENT: \"SQL_INVALID_STATEMENT\",\n SQL_SCHEMA_ERROR: \"SQL_SCHEMA_ERROR\",\n SQL_READONLY_VIOLATION: \"SQL_READONLY_VIOLATION\",\n\n // Storage quota errors\n STORAGE_QUOTA_EXCEEDED: \"STORAGE_QUOTA_EXCEEDED\",\n STORAGE_LIMIT_REACHED: \"STORAGE_LIMIT_REACHED\",\n /** The node answered the usage read without usage numbers (it predates the read). */\n STORAGE_STATUS_UNAVAILABLE: \"STORAGE_STATUS_UNAVAILABLE\",\n\n // DuckDB-specific errors\n DUCKDB_ERROR: \"DUCKDB_ERROR\",\n DUCKDB_PERMISSION_DENIED: \"DUCKDB_PERMISSION_DENIED\",\n DUCKDB_DATABASE_NOT_FOUND: \"DUCKDB_DATABASE_NOT_FOUND\",\n DUCKDB_RESPONSE_TOO_LARGE: \"DUCKDB_RESPONSE_TOO_LARGE\",\n DUCKDB_QUOTA_EXCEEDED: \"DUCKDB_QUOTA_EXCEEDED\",\n DUCKDB_INVALID_STATEMENT: \"DUCKDB_INVALID_STATEMENT\",\n DUCKDB_SCHEMA_ERROR: \"DUCKDB_SCHEMA_ERROR\",\n DUCKDB_READONLY_VIOLATION: \"DUCKDB_READONLY_VIOLATION\",\n} as const;\n\nexport type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];\n\n// =============================================================================\n// Service Session\n// =============================================================================\n\n/**\n * Session data required for authenticated service operations.\n * Both TinyCloudSession and web-sdk Session can be cast to this interface.\n */\nexport interface ServiceSession {\n /** The delegation header containing the UCAN */\n delegationHeader: { Authorization: string };\n /** The delegation CID */\n delegationCid: string;\n /** The space ID for this session */\n spaceId: string;\n /** The verification method DID */\n verificationMethod: string;\n /** The session key JWK (required for invoke) */\n jwk: object;\n}\n\n// =============================================================================\n// Platform Dependencies (Injected)\n// =============================================================================\n\n/**\n * Headers type - compatible with both browser and Node.js.\n */\nexport type ServiceHeaders = Record<string, string> | [string, string][];\n\n/**\n * A single fact object to include in the UCAN invocation.\n * Facts are key-value objects that the server reads from the UCAN facts field.\n */\nexport interface InvocationFact {\n [key: string]: unknown;\n}\n\n/**\n * Facts to include in the UCAN invocation.\n * This is an array of fact objects per the UCAN spec.\n * Used to pass additional parameters that the server reads from the UCAN facts field.\n */\nexport type InvocationFacts = InvocationFact[];\n\n/**\n * Invoke function signature - platform-specific implementation injected via DI.\n * Both node-sdk-wasm and web-sdk-wasm export this with identical signature.\n *\n * @param session - The service session with delegation data\n * @param service - Service name (e.g., \"kv\")\n * @param path - Resource path or key\n * @param action - Action to perform (e.g., \"tinycloud.kv/get\")\n * @param facts - Optional facts to include in the UCAN (e.g., for capabilities/read params)\n * @returns Headers to include in the request\n */\nexport type InvokeFunction = (\n session: ServiceSession,\n service: string,\n path: string,\n action: string,\n facts?: InvocationFacts,\n) => ServiceHeaders;\n\n/**\n * Multi-resource invocation entry.\n */\nexport interface InvokeAnyEntry {\n /**\n * Legacy space-scoped resource. Optional when `resource` is provided.\n */\n spaceId?: string;\n service: string;\n path: string;\n action: string;\n /** Optional raw resource URI. When set, WASM signs this URI directly. */\n resource?: string;\n /** Capability caveats that must remain on this invocation attenuation. */\n caveats?: Record<string, unknown>[];\n}\n\n/**\n * Invoke function for minting a single authorization header that covers\n * multiple capabilities across one effective invoker.\n */\nexport type InvokeAnyFunction = (\n session: ServiceSession,\n entries: InvokeAnyEntry[],\n facts?: InvocationFacts,\n) => ServiceHeaders;\n\n/**\n * Fetch request options - compatible with standard fetch API.\n */\nexport interface FetchRequestInit {\n method?: string;\n headers?: ServiceHeaders;\n body?: Blob | FormData | string;\n signal?: AbortSignal;\n redirect?: \"error\";\n}\n\n/**\n * Fetch response interface - compatible with standard Response.\n */\nexport interface FetchResponse {\n ok: boolean;\n status: number;\n statusText: string;\n body?: unknown;\n headers: {\n get(name: string): string | null;\n };\n json(): Promise<unknown>;\n text(): Promise<string>;\n arrayBuffer(): Promise<ArrayBuffer>;\n blob(): Promise<Blob>;\n /** Present on standard Responses; the retry reads a clone so the caller's body stays intact. */\n clone?(): FetchResponse;\n /** Set by `ServiceContext.invokeWithRetry` on the response it returns: how many requests it took. */\n retry?: RetryStats;\n}\n\n/** What automatic retry cost one request, so latency can be split from backoff. */\nexport interface RetryStats {\n /** Requests sent, including the first (1 means no retry). */\n attempts: number;\n /** Total milliseconds spent waiting between attempts. */\n backoffMs: number;\n}\n\n/**\n * Fetch function signature - allows for custom fetch implementations.\n * Compatible with both browser fetch and Node.js fetch.\n */\nexport type FetchFunction = (\n url: string,\n init?: FetchRequestInit,\n) => Promise<FetchResponse>;\n\n// =============================================================================\n// Retry Policy\n// =============================================================================\n\n/**\n * Automatic retry of node responses that guarantee nothing was applied\n * (`NODE_BUSY`, `REQUEST_TIMEOUT`). Applied once, in the service context's\n * `invokeWithRetry`, which re-mints the signed invocation per attempt. Backoff\n * is full-jitter: each delay is uniform in `[0, step]`, where\n * `step` is the strategy's delay capped at `maxDelayMs`. A node `Retry-After`\n * replaces the delay. Opt out with `{ maxAttempts: 1 }`.\n */\nexport interface RetryPolicy {\n /** Maximum number of attempts (including initial) */\n maxAttempts: number;\n /** Backoff strategy between retries */\n backoff: \"none\" | \"linear\" | \"exponential\";\n /** Base delay in milliseconds for backoff calculation */\n baseDelayMs: number;\n /** Maximum delay in milliseconds between retries */\n maxDelayMs: number;\n /** Node error codes that trigger a retry. `OUTCOME_UNKNOWN` is never retried. */\n retryableErrors: string[];\n /** Total time budget for all attempts and waits, in milliseconds. Unset: no cap. */\n maxTotalMs?: number;\n}\n\n/**\n * Default retry policy.\n */\nexport const defaultRetryPolicy: RetryPolicy = {\n maxAttempts: 4,\n backoff: \"exponential\",\n baseDelayMs: 250,\n maxDelayMs: 4000,\n retryableErrors: [ErrorCodes.NODE_BUSY, ErrorCodes.REQUEST_TIMEOUT],\n maxTotalMs: 12000,\n};\n\n// =============================================================================\n// Service Context\n// =============================================================================\n\n/**\n * Event handler function type.\n */\nexport type EventHandler = (data: unknown) => void;\n\n/**\n * SDK telemetry event handler.\n */\nexport type TelemetryEventHandler = (event: string, data: unknown) => void;\n\n/**\n * Default-off telemetry configuration.\n */\nexport type TelemetryConfig =\n | boolean\n | {\n enabled?: boolean;\n onEvent?: TelemetryEventHandler;\n };\n\n/**\n * Service interface - base contract for all services.\n */\nexport interface IService {\n /** Initialize service with context */\n initialize(context: IServiceContext): void;\n\n /** Called when session changes (sign-in, sign-out, refresh) */\n onSessionChange(session: ServiceSession | null): void;\n\n /** Called when SDK signs out - should abort pending operations */\n onSignOut(): void;\n\n /** Service-specific configuration */\n readonly config: Record<string, unknown>;\n}\n\n/**\n * Context provided to services for accessing platform dependencies.\n * The SDK creates this context and passes it to services during initialization.\n */\nexport interface IServiceContext {\n // Session management\n /** Current active session, or null if not authenticated */\n readonly session: ServiceSession | null;\n /** Whether there is an active authenticated session */\n readonly isAuthenticated: boolean;\n\n // Platform dependencies (injected by SDK)\n /** Platform-specific invoke function from WASM binding */\n readonly invoke: InvokeFunction;\n /** Optional multi-resource invoke function */\n readonly invokeAny?: InvokeAnyFunction;\n /** Fetch function (defaults to globalThis.fetch) */\n readonly fetch: FetchFunction;\n /** Send a signed invocation, retrying a busy node; `build` re-signs per attempt. Absent: sends once. */\n invokeWithRetry?(\n url: string,\n build: () => FetchRequestInit | Promise<FetchRequestInit>,\n ): Promise<FetchResponse>;\n /** Available TinyCloud host URLs */\n readonly hosts: string[];\n\n // Cross-service access\n /** Get another registered service by name */\n getService<T extends IService>(name: string): T | undefined;\n\n // Telemetry/Events\n /** Emit a telemetry event */\n emit(event: string, data: unknown): void;\n /** Subscribe to events */\n on(event: string, handler: EventHandler): () => void;\n\n // Lifecycle\n /** Abort signal that fires when SDK signs out. */\n readonly abortSignal: AbortSignal;\n /** Additional abort signal for a scoped operation, when active. */\n readonly operationAbortSignal?: AbortSignal;\n /** Install an additional signal for the duration of a scoped operation. */\n setOperationAbortSignal?(signal: AbortSignal | undefined): void;\n // Retry policy\n /** Retry policy for failed requests */\n readonly retryPolicy: RetryPolicy;\n}\n\n// =============================================================================\n// Telemetry Events\n// =============================================================================\n\n/**\n * Event emitted before a service request.\n */\nexport interface ServiceRequestEvent {\n service: string;\n action: string;\n span?: string;\n key?: string;\n timestamp: number;\n}\n\n/**\n * Event emitted after a service response.\n */\nexport interface ServiceResponseEvent {\n service: string;\n action: string;\n span?: string;\n ok: boolean;\n duration: number;\n durationMs?: number;\n status?: number;\n}\n\n/**\n * Event emitted on service error.\n */\nexport interface ServiceErrorEvent {\n service: string;\n span?: string;\n error: ServiceError;\n}\n\n/**\n * Event emitted on retry attempt.\n */\nexport interface ServiceRetryEvent {\n service: string;\n attempt: number;\n maxAttempts: number;\n error: ServiceError;\n}\n\n/**\n * Generic named span event for aggregating operation timings.\n */\nexport interface TelemetrySpanEvent {\n span: string;\n ok: boolean;\n durationMs: number;\n service?: string;\n action?: string;\n status?: number;\n error?: ServiceError;\n}\n\n/**\n * Telemetry event names.\n */\nexport const TelemetryEvents = {\n SPAN: \"telemetry.span\",\n SERVICE_REQUEST: \"service.request\",\n SERVICE_RESPONSE: \"service.response\",\n SERVICE_ERROR: \"service.error\",\n SERVICE_RETRY: \"service.retry\",\n SESSION_CHANGED: \"session.changed\",\n SESSION_EXPIRED: \"session.expired\",\n REPLICATION_READ: \"replication.read\",\n REPLICATION_WRITE: \"replication.write\",\n REPLICATION_SYNC: \"replication.sync\",\n REPLICATION_STATE: \"replication.state\",\n REPLICATION_DIVERGENCE: \"replication.divergence\",\n REPLICATION_GRANT: \"replication.grant\",\n REPLICATION_LIFECYCLE: \"replication.lifecycle\",\n /** Emitted once, on the first write rejected because storage is full. */\n STORAGE_FULL: \"storage.full\",\n} as const;\n\n// =============================================================================\n// Helper Functions\n// =============================================================================\n\n/**\n * Create a success result.\n */\nexport function ok<T>(data: T): Result<T> {\n return { ok: true, data };\n}\n\n/**\n * Create an error result.\n */\nexport function err<E = ServiceError>(error: E): Result<never, E> {\n return { ok: false, error };\n}\n\n/**\n * Create a ServiceError.\n */\nexport function serviceError(\n code: string,\n message: string,\n service: string,\n options?: { cause?: Error; meta?: Record<string, unknown> },\n): ServiceError {\n return {\n code,\n message,\n service,\n cause: options?.cause,\n meta: options?.meta,\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -26,6 +26,10 @@ var ErrorCodes = {
|
|
|
26
26
|
/** HTTP 401: a delegation the cited one rests on was revoked. */
|
|
27
27
|
AUTH_DELEGATION_ANCESTOR_REVOKED: "AUTH_DELEGATION_ANCESTOR_REVOKED",
|
|
28
28
|
NETWORK_ERROR: "NETWORK_ERROR",
|
|
29
|
+
NODE_NOT_RESPONDING: "NODE_NOT_RESPONDING",
|
|
30
|
+
NODE_IDENTITY_MISMATCH: "NODE_IDENTITY_MISMATCH",
|
|
31
|
+
PARTIAL_NODE_WRITE: "PARTIAL_NODE_WRITE",
|
|
32
|
+
RESOURCE_NOT_ROUTED: "RESOURCE_NOT_ROUTED",
|
|
29
33
|
TIMEOUT: "TIMEOUT",
|
|
30
34
|
ABORTED: "ABORTED",
|
|
31
35
|
INVALID_INPUT: "INVALID_INPUT",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/types.ts","../../src/telemetry/requestLog.ts"],"sourcesContent":["/**\n * SDK Services - Core Types\n *\n * These types define the service architecture for TinyCloud SDK.\n * Services use dependency injection via IServiceContext for platform independence.\n */\n\n// =============================================================================\n// Result Type Pattern\n// =============================================================================\n\n/**\n * Result type for service operations.\n * Services return Result instead of throwing, making error handling explicit.\n *\n * @template T - The success data type\n * @template E - The error type (defaults to ServiceError)\n *\n * @example\n * ```typescript\n * const result = await kv.get('key');\n * if (result.ok) {\n * console.log(result.data);\n * } else {\n * console.error(result.error.code);\n * }\n * ```\n */\nexport type Result<T, E = ServiceError> =\n | { ok: true; data: T }\n | { ok: false; error: E };\n\n/**\n * Service error with structured information.\n */\nexport interface ServiceError {\n /** Error code for programmatic handling (e.g., 'KV_NOT_FOUND', 'AUTH_EXPIRED') */\n code: string;\n /** Human-readable error message */\n message: string;\n /** Service that produced the error (e.g., 'kv', 'sql') */\n service: string;\n /** Original error if this wraps another error */\n cause?: Error;\n /** Additional metadata about the error */\n meta?: Record<string, unknown>;\n}\n\n/**\n * A node-supplied permission hint after strict SDK validation. This is\n * deliberately narrower than a general capability: no wildcards, caveats,\n * descriptions, or transport metadata can cross the service boundary.\n */\nexport interface PermissionHint {\n readonly service: \"tinycloud.kv\" | \"tinycloud.encryption\";\n readonly space?: string;\n readonly path: string;\n readonly actions: readonly string[];\n}\n\n/**\n * Storage quota information returned with quota-related errors.\n * @deprecated Read `usedBytes`, `limitBytes` and `account` from the error's\n * `meta`, or use `StorageFullEvent` from `tc.on(\"storage.full\", handler)`.\n */\nexport interface StorageQuotaInfo {\n usedBytes: number;\n limitBytes: number;\n service: string;\n}\n\n/**\n * Standard error codes used across services.\n */\nexport const ErrorCodes = {\n // Common errors\n NOT_FOUND: \"NOT_FOUND\",\n AUTH_EXPIRED: \"AUTH_EXPIRED\",\n AUTH_REQUIRED: \"AUTH_REQUIRED\",\n AUTH_UNAUTHORIZED: \"AUTH_UNAUTHORIZED\",\n /** HTTP 401: the delegation the invocation cites was revoked. */\n AUTH_DELEGATION_REVOKED: \"AUTH_DELEGATION_REVOKED\",\n /** HTTP 401: a delegation the cited one rests on was revoked. */\n AUTH_DELEGATION_ANCESTOR_REVOKED: \"AUTH_DELEGATION_ANCESTOR_REVOKED\",\n NETWORK_ERROR: \"NETWORK_ERROR\",\n TIMEOUT: \"TIMEOUT\",\n ABORTED: \"ABORTED\",\n INVALID_INPUT: \"INVALID_INPUT\",\n PERMISSION_DENIED: \"PERMISSION_DENIED\",\n\n // Node retry signals (tinycloud-node #347). `meta.retryable` says whether a\n // retry is safe; `meta.retryAfterMs` carries the node's Retry-After.\n /** HTTP 503 `sql_busy:<stage>` / `unavailable_retryable`/`read_audit_busy`: nothing was applied; retry after `meta.retryAfterMs`. */\n NODE_BUSY: \"NODE_BUSY\",\n /** HTTP 503 `request_timeout`: the node's deadline passed before any write began; nothing was applied. */\n REQUEST_TIMEOUT: \"REQUEST_TIMEOUT\",\n /** HTTP 500 `outcome_unknown`: a write may have been applied; read back before retrying a non-idempotent operation. */\n OUTCOME_UNKNOWN: \"OUTCOME_UNKNOWN\",\n\n // KV-specific errors\n KV_NOT_FOUND: \"KV_NOT_FOUND\",\n KV_WRITE_FAILED: \"KV_WRITE_FAILED\",\n KV_PRECONDITION_FAILED: \"KV_PRECONDITION_FAILED\",\n KV_CONFLICT: \"KV_CONFLICT\",\n KV_RESPONSE_TOO_LARGE: \"KV_RESPONSE_TOO_LARGE\",\n /** HTTP 410 from the change feed: discard sync state and restart without a cursor; `meta.reason` says why. */\n KV_SYNC_RESET_REQUIRED: \"KV_SYNC_RESET_REQUIRED\",\n /** HTTP 403 from the change feed: the retention grant was refused; `meta.reason` says why. */\n KV_RETENTION_GRANT_REFUSED: \"KV_RETENTION_GRANT_REFUSED\",\n\n // SQL-specific errors\n SQL_ERROR: \"SQL_ERROR\",\n SQL_PERMISSION_DENIED: \"SQL_PERMISSION_DENIED\",\n SQL_DATABASE_NOT_FOUND: \"SQL_DATABASE_NOT_FOUND\",\n SQL_RESPONSE_TOO_LARGE: \"SQL_RESPONSE_TOO_LARGE\",\n SQL_QUOTA_EXCEEDED: \"SQL_QUOTA_EXCEEDED\",\n SQL_INVALID_STATEMENT: \"SQL_INVALID_STATEMENT\",\n SQL_SCHEMA_ERROR: \"SQL_SCHEMA_ERROR\",\n SQL_READONLY_VIOLATION: \"SQL_READONLY_VIOLATION\",\n\n // Storage quota errors\n STORAGE_QUOTA_EXCEEDED: \"STORAGE_QUOTA_EXCEEDED\",\n STORAGE_LIMIT_REACHED: \"STORAGE_LIMIT_REACHED\",\n /** The node answered the usage read without usage numbers (it predates the read). */\n STORAGE_STATUS_UNAVAILABLE: \"STORAGE_STATUS_UNAVAILABLE\",\n\n // DuckDB-specific errors\n DUCKDB_ERROR: \"DUCKDB_ERROR\",\n DUCKDB_PERMISSION_DENIED: \"DUCKDB_PERMISSION_DENIED\",\n DUCKDB_DATABASE_NOT_FOUND: \"DUCKDB_DATABASE_NOT_FOUND\",\n DUCKDB_RESPONSE_TOO_LARGE: \"DUCKDB_RESPONSE_TOO_LARGE\",\n DUCKDB_QUOTA_EXCEEDED: \"DUCKDB_QUOTA_EXCEEDED\",\n DUCKDB_INVALID_STATEMENT: \"DUCKDB_INVALID_STATEMENT\",\n DUCKDB_SCHEMA_ERROR: \"DUCKDB_SCHEMA_ERROR\",\n DUCKDB_READONLY_VIOLATION: \"DUCKDB_READONLY_VIOLATION\",\n} as const;\n\nexport type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];\n\n// =============================================================================\n// Service Session\n// =============================================================================\n\n/**\n * Session data required for authenticated service operations.\n * Both TinyCloudSession and web-sdk Session can be cast to this interface.\n */\nexport interface ServiceSession {\n /** The delegation header containing the UCAN */\n delegationHeader: { Authorization: string };\n /** The delegation CID */\n delegationCid: string;\n /** The space ID for this session */\n spaceId: string;\n /** The verification method DID */\n verificationMethod: string;\n /** The session key JWK (required for invoke) */\n jwk: object;\n}\n\n// =============================================================================\n// Platform Dependencies (Injected)\n// =============================================================================\n\n/**\n * Headers type - compatible with both browser and Node.js.\n */\nexport type ServiceHeaders = Record<string, string> | [string, string][];\n\n/**\n * A single fact object to include in the UCAN invocation.\n * Facts are key-value objects that the server reads from the UCAN facts field.\n */\nexport interface InvocationFact {\n [key: string]: unknown;\n}\n\n/**\n * Facts to include in the UCAN invocation.\n * This is an array of fact objects per the UCAN spec.\n * Used to pass additional parameters that the server reads from the UCAN facts field.\n */\nexport type InvocationFacts = InvocationFact[];\n\n/**\n * Invoke function signature - platform-specific implementation injected via DI.\n * Both node-sdk-wasm and web-sdk-wasm export this with identical signature.\n *\n * @param session - The service session with delegation data\n * @param service - Service name (e.g., \"kv\")\n * @param path - Resource path or key\n * @param action - Action to perform (e.g., \"tinycloud.kv/get\")\n * @param facts - Optional facts to include in the UCAN (e.g., for capabilities/read params)\n * @returns Headers to include in the request\n */\nexport type InvokeFunction = (\n session: ServiceSession,\n service: string,\n path: string,\n action: string,\n facts?: InvocationFacts,\n) => ServiceHeaders;\n\n/**\n * Multi-resource invocation entry.\n */\nexport interface InvokeAnyEntry {\n /**\n * Legacy space-scoped resource. Optional when `resource` is provided.\n */\n spaceId?: string;\n service: string;\n path: string;\n action: string;\n /** Optional raw resource URI. When set, WASM signs this URI directly. */\n resource?: string;\n /** Capability caveats that must remain on this invocation attenuation. */\n caveats?: Record<string, unknown>[];\n}\n\n/**\n * Invoke function for minting a single authorization header that covers\n * multiple capabilities across one effective invoker.\n */\nexport type InvokeAnyFunction = (\n session: ServiceSession,\n entries: InvokeAnyEntry[],\n facts?: InvocationFacts,\n) => ServiceHeaders;\n\n/**\n * Fetch request options - compatible with standard fetch API.\n */\nexport interface FetchRequestInit {\n method?: string;\n headers?: ServiceHeaders;\n body?: Blob | FormData | string;\n signal?: AbortSignal;\n redirect?: \"error\";\n}\n\n/**\n * Fetch response interface - compatible with standard Response.\n */\nexport interface FetchResponse {\n ok: boolean;\n status: number;\n statusText: string;\n body?: unknown;\n headers: {\n get(name: string): string | null;\n };\n json(): Promise<unknown>;\n text(): Promise<string>;\n arrayBuffer(): Promise<ArrayBuffer>;\n blob(): Promise<Blob>;\n /** Present on standard Responses; the retry reads a clone so the caller's body stays intact. */\n clone?(): FetchResponse;\n /** Set by `ServiceContext.invokeWithRetry` on the response it returns: how many requests it took. */\n retry?: RetryStats;\n}\n\n/** What automatic retry cost one request, so latency can be split from backoff. */\nexport interface RetryStats {\n /** Requests sent, including the first (1 means no retry). */\n attempts: number;\n /** Total milliseconds spent waiting between attempts. */\n backoffMs: number;\n}\n\n/**\n * Fetch function signature - allows for custom fetch implementations.\n * Compatible with both browser fetch and Node.js fetch.\n */\nexport type FetchFunction = (\n url: string,\n init?: FetchRequestInit,\n) => Promise<FetchResponse>;\n\n// =============================================================================\n// Retry Policy\n// =============================================================================\n\n/**\n * Automatic retry of node responses that guarantee nothing was applied\n * (`NODE_BUSY`, `REQUEST_TIMEOUT`). Applied once, in the service context's\n * `invokeWithRetry`, which re-mints the signed invocation per attempt. Backoff\n * is full-jitter: each delay is uniform in `[0, step]`, where\n * `step` is the strategy's delay capped at `maxDelayMs`. A node `Retry-After`\n * replaces the delay. Opt out with `{ maxAttempts: 1 }`.\n */\nexport interface RetryPolicy {\n /** Maximum number of attempts (including initial) */\n maxAttempts: number;\n /** Backoff strategy between retries */\n backoff: \"none\" | \"linear\" | \"exponential\";\n /** Base delay in milliseconds for backoff calculation */\n baseDelayMs: number;\n /** Maximum delay in milliseconds between retries */\n maxDelayMs: number;\n /** Node error codes that trigger a retry. `OUTCOME_UNKNOWN` is never retried. */\n retryableErrors: string[];\n /** Total time budget for all attempts and waits, in milliseconds. Unset: no cap. */\n maxTotalMs?: number;\n}\n\n/**\n * Default retry policy.\n */\nexport const defaultRetryPolicy: RetryPolicy = {\n maxAttempts: 4,\n backoff: \"exponential\",\n baseDelayMs: 250,\n maxDelayMs: 4000,\n retryableErrors: [ErrorCodes.NODE_BUSY, ErrorCodes.REQUEST_TIMEOUT],\n maxTotalMs: 12000,\n};\n\n// =============================================================================\n// Service Context\n// =============================================================================\n\n/**\n * Event handler function type.\n */\nexport type EventHandler = (data: unknown) => void;\n\n/**\n * SDK telemetry event handler.\n */\nexport type TelemetryEventHandler = (event: string, data: unknown) => void;\n\n/**\n * Default-off telemetry configuration.\n */\nexport type TelemetryConfig =\n | boolean\n | {\n enabled?: boolean;\n onEvent?: TelemetryEventHandler;\n };\n\n/**\n * Service interface - base contract for all services.\n */\nexport interface IService {\n /** Initialize service with context */\n initialize(context: IServiceContext): void;\n\n /** Called when session changes (sign-in, sign-out, refresh) */\n onSessionChange(session: ServiceSession | null): void;\n\n /** Called when SDK signs out - should abort pending operations */\n onSignOut(): void;\n\n /** Service-specific configuration */\n readonly config: Record<string, unknown>;\n}\n\n/**\n * Context provided to services for accessing platform dependencies.\n * The SDK creates this context and passes it to services during initialization.\n */\nexport interface IServiceContext {\n // Session management\n /** Current active session, or null if not authenticated */\n readonly session: ServiceSession | null;\n /** Whether there is an active authenticated session */\n readonly isAuthenticated: boolean;\n\n // Platform dependencies (injected by SDK)\n /** Platform-specific invoke function from WASM binding */\n readonly invoke: InvokeFunction;\n /** Optional multi-resource invoke function */\n readonly invokeAny?: InvokeAnyFunction;\n /** Fetch function (defaults to globalThis.fetch) */\n readonly fetch: FetchFunction;\n /** Send a signed invocation, retrying a busy node; `build` re-signs per attempt. Absent: sends once. */\n invokeWithRetry?(\n url: string,\n build: () => FetchRequestInit | Promise<FetchRequestInit>,\n ): Promise<FetchResponse>;\n /** Available TinyCloud host URLs */\n readonly hosts: string[];\n\n // Cross-service access\n /** Get another registered service by name */\n getService<T extends IService>(name: string): T | undefined;\n\n // Telemetry/Events\n /** Emit a telemetry event */\n emit(event: string, data: unknown): void;\n /** Subscribe to events */\n on(event: string, handler: EventHandler): () => void;\n\n // Lifecycle\n /** Abort signal that fires when SDK signs out. */\n readonly abortSignal: AbortSignal;\n /** Additional abort signal for a scoped operation, when active. */\n readonly operationAbortSignal?: AbortSignal;\n /** Install an additional signal for the duration of a scoped operation. */\n setOperationAbortSignal?(signal: AbortSignal | undefined): void;\n // Retry policy\n /** Retry policy for failed requests */\n readonly retryPolicy: RetryPolicy;\n}\n\n// =============================================================================\n// Telemetry Events\n// =============================================================================\n\n/**\n * Event emitted before a service request.\n */\nexport interface ServiceRequestEvent {\n service: string;\n action: string;\n span?: string;\n key?: string;\n timestamp: number;\n}\n\n/**\n * Event emitted after a service response.\n */\nexport interface ServiceResponseEvent {\n service: string;\n action: string;\n span?: string;\n ok: boolean;\n duration: number;\n durationMs?: number;\n status?: number;\n}\n\n/**\n * Event emitted on service error.\n */\nexport interface ServiceErrorEvent {\n service: string;\n span?: string;\n error: ServiceError;\n}\n\n/**\n * Event emitted on retry attempt.\n */\nexport interface ServiceRetryEvent {\n service: string;\n attempt: number;\n maxAttempts: number;\n error: ServiceError;\n}\n\n/**\n * Generic named span event for aggregating operation timings.\n */\nexport interface TelemetrySpanEvent {\n span: string;\n ok: boolean;\n durationMs: number;\n service?: string;\n action?: string;\n status?: number;\n error?: ServiceError;\n}\n\n/**\n * Telemetry event names.\n */\nexport const TelemetryEvents = {\n SPAN: \"telemetry.span\",\n SERVICE_REQUEST: \"service.request\",\n SERVICE_RESPONSE: \"service.response\",\n SERVICE_ERROR: \"service.error\",\n SERVICE_RETRY: \"service.retry\",\n SESSION_CHANGED: \"session.changed\",\n SESSION_EXPIRED: \"session.expired\",\n REPLICATION_READ: \"replication.read\",\n REPLICATION_WRITE: \"replication.write\",\n REPLICATION_SYNC: \"replication.sync\",\n REPLICATION_STATE: \"replication.state\",\n REPLICATION_DIVERGENCE: \"replication.divergence\",\n REPLICATION_GRANT: \"replication.grant\",\n REPLICATION_LIFECYCLE: \"replication.lifecycle\",\n /** Emitted once, on the first write rejected because storage is full. */\n STORAGE_FULL: \"storage.full\",\n} as const;\n\n// =============================================================================\n// Helper Functions\n// =============================================================================\n\n/**\n * Create a success result.\n */\nexport function ok<T>(data: T): Result<T> {\n return { ok: true, data };\n}\n\n/**\n * Create an error result.\n */\nexport function err<E = ServiceError>(error: E): Result<never, E> {\n return { ok: false, error };\n}\n\n/**\n * Create a ServiceError.\n */\nexport function serviceError(\n code: string,\n message: string,\n service: string,\n options?: { cause?: Error; meta?: Record<string, unknown> },\n): ServiceError {\n return {\n code,\n message,\n service,\n cause: options?.cause,\n meta: options?.meta,\n };\n}\n","/**\n * Developer request log core. Events are sanitized by `buildRequestEvent`, the\n * only constructor, which copies an allowlist of fields. Values, headers,\n * bodies, tokens, delegations, and error text have no field to land in.\n */\n\nimport { TelemetryEvents, type IServiceContext } from \"../types\";\n\nexport type RequestService =\n | \"kv\"\n | \"sql\"\n | \"duckdb\"\n | \"hooks\"\n | \"vault\"\n | \"space\"\n | \"sync\"\n | \"other\";\n\nexport interface RequestEvent {\n v: 1;\n /** Monotonic per RequestLog. */\n id: number;\n /** Request start, ms since epoch. */\n at: number;\n service: RequestService;\n /** `service/action`, e.g. `kv/get`; `other` when unknown. */\n op: string;\n /** `first/` of the key (`[redacted]` when unsafe or slashless); full key only for allowlisted first segments. */\n path: string | null;\n /** Tail label of the space id, never the id. */\n space: string | null;\n /** URL host[:port] only. */\n host: string | null;\n /** Body bytes for writes, response bytes for reads; null when unknown. */\n bytes: number | null;\n /** Whole logical request, including retry backoff; null when unknown. */\n durationMs: number | null;\n attempts: number;\n ok: boolean;\n code: string;\n source: \"network\" | \"replica\";\n}\n\n/** A sanitized event before the log assigns its id. */\nexport type RequestDraft = Omit<RequestEvent, \"id\">;\n\nexport interface RequestLogReader {\n /** Copy, oldest first. */\n snapshot(): readonly RequestEvent[];\n subscribe(cb: (event: RequestEvent) => void): () => void;\n onClear(cb: () => void): () => void;\n}\n\n/**\n * The fetch layer's only view of the developer request log. A context without a\n * sink never links the sanitizer or the invocation-meta store.\n */\nexport interface RequestSink {\n record(event: RequestDraft): void;\n /** Derive and record one event from one transport attempt's raw facts. */\n observe(observation: FetchObservation): void;\n /** Attach service, path, and action to headers built for an invocation. */\n remember(headers: object, meta: InvocationMeta): void;\n}\n\n/** Transport facts for one attempt, read by `observeFetch`. */\nexport interface FetchObservation {\n url: string;\n requestHeaders: unknown;\n requestBody: unknown;\n responseContentLength: string | null | undefined;\n ok: boolean;\n code: string;\n durationMs: number;\n attempts: number;\n at: number;\n}\n\n/** Raw facts about one logical request. Only `buildRequestEvent` reads them. */\nexport interface RawRequest {\n service?: string;\n op?: string;\n /** Unscrubbed key path. */\n path?: string | null;\n /** Full space id; only its tail label is kept. */\n spaceId?: string | null;\n url?: string | null;\n bytes?: number | null;\n durationMs: number | null;\n attempts?: number;\n ok: boolean;\n code?: string;\n source?: string;\n at: number;\n}\n\nconst RING_SIZE = 500;\nconst MAX_PATH = 256;\nconst REDACTED = \"[redacted]\";\n\nconst SERVICE_NAME = /^(kv|sql|duckdb|hooks|vault|space|sync)$/;\nconst OP_SHAPE = /^[a-z]+\\/[A-Za-z]+$/;\nconst SPACE_LABEL = /^[a-z0-9][a-z0-9_-]{0,31}$/;\nconst CODE_SHAPE = /^(ok|HTTP_[1-5]\\d\\d|[A-Z][A-Z0-9_]{1,63}|ERROR)$/;\nconst PATH_LABEL = /^[a-z0-9][a-z0-9_-]{0,31}$/;\n/** First segments whose full key may be recorded. Empty: only `first/` is ever kept. */\nconst PATH_ALLOWLIST: ReadonlySet<string> = new Set<string>();\n\nfunction scrubPath(raw: string | null | undefined): string | null {\n if (typeof raw !== \"string\") return null;\n const bare = raw.split(/[?#]/, 1)[0] ?? \"\";\n if (bare === \"\") return null;\n let decoded: string;\n try {\n decoded = decodeURIComponent(bare);\n } catch {\n return REDACTED;\n }\n const stripped = decoded.replace(/^\\/+/, \"\").replace(/\\/+/g, \"/\");\n if (stripped === \"\") return null;\n const slash = stripped.indexOf(\"/\");\n // Slashless: the whole value may be a bare secret name.\n if (slash === -1) return REDACTED;\n const first = stripped.slice(0, slash).toLowerCase();\n if (!PATH_LABEL.test(first)) return REDACTED;\n if (PATH_ALLOWLIST.has(first)) return stripped.length > MAX_PATH ? stripped.slice(0, MAX_PATH) : stripped;\n return `${first}/`;\n}\n\nfunction spaceLabel(spaceId: string | null | undefined): string | null {\n if (typeof spaceId !== \"string\" || spaceId === \"\") return null;\n // Only the name after the last colon is kept; an id without a colon has no such name.\n const colon = spaceId.lastIndexOf(\":\");\n if (colon === -1) return \"space\";\n const tail = spaceId.slice(colon + 1);\n return SPACE_LABEL.test(tail) ? tail : \"space\";\n}\n\nfunction hostOf(url: string | null | undefined): string | null {\n if (typeof url !== \"string\") return null;\n try {\n return new URL(url).host || null;\n } catch {\n return null;\n }\n}\n\n/** UTF-8 byte length of a request body without consuming it; null when unknown. */\nexport function byteLengthOf(body: unknown): number | null {\n if (typeof body === \"string\") return new TextEncoder().encode(body).byteLength;\n if (typeof Blob !== \"undefined\" && body instanceof Blob) return body.size;\n if (body instanceof ArrayBuffer) return body.byteLength;\n if (ArrayBuffer.isView(body)) return body.byteLength;\n return null;\n}\n\n/** Response Content-Length as bytes; null unless the header is plain digits (blank, signed, or exponent forms are unknown). */\nexport function declaredLengthOf(header: string | null | undefined): number | null {\n if (typeof header !== \"string\") return null;\n const value = header.trim();\n return /^\\d+$/.test(value) ? Number(value) : null;\n}\n\n/** What the signing path knows about a request; read back by the fetch layer. */\nexport interface InvocationMeta {\n service: string;\n path: string;\n action: string;\n spaceId?: string | null;\n}\n\nconst invocationMeta = new WeakMap<object, InvocationMeta>();\n\nexport function invocationMetaOf(headers: unknown): InvocationMeta | undefined {\n return typeof headers === \"object\" && headers !== null ? invocationMeta.get(headers) : undefined;\n}\n\nexport function buildRequestEvent(raw: RawRequest): RequestDraft {\n const service: RequestService =\n raw.service !== undefined && SERVICE_NAME.test(raw.service)\n ? (raw.service as RequestService)\n : \"other\";\n const space = spaceLabel(raw.spaceId);\n const op = typeof raw.op === \"string\" && OP_SHAPE.test(raw.op) ? raw.op : \"other\";\n const code =\n raw.code === undefined ? (raw.ok ? \"ok\" : \"ERROR\") : CODE_SHAPE.test(raw.code) ? raw.code : \"ERROR\";\n const bytes =\n typeof raw.bytes === \"number\" && Number.isFinite(raw.bytes) && raw.bytes >= 0\n ? Math.floor(raw.bytes)\n : null;\n return {\n v: 1,\n at: raw.at,\n service,\n op,\n path: scrubPath(raw.path),\n space,\n host: hostOf(raw.url),\n bytes,\n durationMs: typeof raw.durationMs === \"number\" && Number.isFinite(raw.durationMs) ? Math.max(0, Math.round(raw.durationMs)) : null,\n attempts:\n raw.attempts !== undefined && Number.isFinite(raw.attempts) && raw.attempts >= 1\n ? Math.floor(raw.attempts)\n : 1,\n ok: raw.ok === true,\n code,\n source: raw.source === \"replica\" ? \"replica\" : \"network\",\n };\n}\n\n/**\n * Derives one event from one transport attempt. Invocation metadata comes from the\n * request headers. Bytes are the request body length when a body was sent (null for\n * streams); with no request body, the declared response length.\n */\nexport function observeFetch(observation: FetchObservation, emit: (event: RequestDraft) => void): void {\n const meta = invocationMetaOf(observation.requestHeaders);\n emit(\n buildRequestEvent({\n service: meta?.service ?? \"other\",\n op: meta ? `${meta.service}/${meta.action.split(\"/\").pop() ?? meta.action}` : \"other\",\n path: meta?.path ?? null,\n spaceId: meta?.spaceId ?? null,\n url: observation.url,\n bytes: requestBytesOf(observation.requestBody, observation.responseContentLength),\n durationMs: observation.durationMs,\n attempts: observation.attempts,\n ok: observation.ok,\n code: observation.code,\n at: observation.at,\n }),\n );\n}\n\n/**\n * Request bytes: the body length when a body was sent (null when it cannot be measured\n * without consuming it); with no body, the declared response Content-Length.\n */\nexport function requestBytesOf(body: unknown, responseContentLength: string | null | undefined): number | null {\n if (body === undefined || body === null) return declaredLengthOf(responseContentLength);\n return byteLengthOf(body);\n}\n\n/**\n * Maps replica-served KV reads (`replication.read` with `source: \"replica\"`) to\n * request events. Network reads and replication writes are not mapped here:\n * their wire requests are recorded by the fetch layer.\n */\nexport function observeReplicaReads(context: IServiceContext, sink: RequestSink): () => void {\n return context.on(TelemetryEvents.REPLICATION_READ, (data: unknown) => {\n try {\n const read = (typeof data === \"object\" && data !== null ? data : {}) as Record<string, unknown>;\n if (read.source !== \"replica\") return;\n sink.record(\n buildRequestEvent({\n service: \"kv\",\n op: `kv/${String(read.op)}`,\n path: typeof read.key === \"string\" ? read.key : null,\n spaceId: typeof read.space === \"string\" ? read.space : null,\n url: null,\n bytes: null,\n durationMs: typeof read.latencyMs === \"number\" ? read.latencyMs : null,\n attempts: 1,\n ok: read.outcome !== \"error\",\n code: read.outcome === \"not_found\" ? \"KV_NOT_FOUND\" : typeof read.code === \"string\" ? read.code : undefined,\n source: \"replica\",\n at: Date.now(),\n }),\n );\n } catch {\n // Observability must not affect KV behavior.\n }\n });\n}\n\ntype Listener<T extends unknown[]> = (...args: T) => void;\n\n/** Fixed-size in-memory ring of sanitized events. Lost on reload by design (v1). */\nexport class RequestLog implements RequestSink, RequestLogReader {\n readonly #slots: (RequestEvent | undefined)[] = new Array(RING_SIZE);\n #head = 0;\n #count = 0;\n #nextId = 1;\n readonly #subscribers = new Set<Listener<[RequestEvent]>>();\n readonly #clearListeners = new Set<Listener<[]>>();\n\n record(draft: RequestDraft): void {\n const event: RequestEvent = Object.freeze({ ...draft, id: this.#nextId++ });\n this.#slots[this.#head] = event;\n this.#head = (this.#head + 1) % RING_SIZE;\n if (this.#count < RING_SIZE) this.#count++;\n for (const cb of this.#subscribers) {\n try {\n cb(event);\n } catch {\n // A failing subscriber must not break the request path or other subscribers.\n }\n }\n }\n\n /** Called only when a sink is set, so developer-off code never writes here. */\n remember(headers: object, meta: InvocationMeta): void {\n invocationMeta.set(headers, meta);\n }\n\n observe(observation: FetchObservation): void {\n observeFetch(observation, (event) => this.record(event));\n }\n\n snapshot(): RequestEvent[] {\n const start = this.#count < RING_SIZE ? 0 : this.#head;\n const out: RequestEvent[] = [];\n for (let i = 0; i < this.#count; i++) {\n out.push(this.#slots[(start + i) % RING_SIZE]!);\n }\n return out;\n }\n\n subscribe(cb: (event: RequestEvent) => void): () => void {\n this.#subscribers.add(cb);\n return () => {\n this.#subscribers.delete(cb);\n };\n }\n\n onClear(cb: () => void): () => void {\n this.#clearListeners.add(cb);\n return () => {\n this.#clearListeners.delete(cb);\n };\n }\n\n clear(): void {\n this.#slots.fill(undefined);\n this.#head = 0;\n this.#count = 0;\n for (const cb of this.#clearListeners) {\n try {\n cb();\n } catch {\n // Isolated like subscribers.\n }\n }\n }\n}\n\n/** The public view of a log: reads and subscriptions only. `record` and `clear` stay with the owner. */\nexport function readOnlyRequestLog(log: RequestLog): RequestLogReader {\n return Object.freeze({\n snapshot: () => log.snapshot(),\n subscribe: (cb: (event: RequestEvent) => void) => log.subscribe(cb),\n onClear: (cb: () => void) => log.onClear(cb),\n });\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/types.ts","../../src/telemetry/requestLog.ts"],"sourcesContent":["/**\n * SDK Services - Core Types\n *\n * These types define the service architecture for TinyCloud SDK.\n * Services use dependency injection via IServiceContext for platform independence.\n */\n\n// =============================================================================\n// Result Type Pattern\n// =============================================================================\n\n/**\n * Result type for service operations.\n * Services return Result instead of throwing, making error handling explicit.\n *\n * @template T - The success data type\n * @template E - The error type (defaults to ServiceError)\n *\n * @example\n * ```typescript\n * const result = await kv.get('key');\n * if (result.ok) {\n * console.log(result.data);\n * } else {\n * console.error(result.error.code);\n * }\n * ```\n */\nexport type Result<T, E = ServiceError> =\n | { ok: true; data: T }\n | { ok: false; error: E };\n\n/**\n * Service error with structured information.\n */\nexport interface ServiceError {\n /** Error code for programmatic handling (e.g., 'KV_NOT_FOUND', 'AUTH_EXPIRED') */\n code: string;\n /** Human-readable error message */\n message: string;\n /** Service that produced the error (e.g., 'kv', 'sql') */\n service: string;\n /** Original error if this wraps another error */\n cause?: Error;\n /** Additional metadata about the error */\n meta?: Record<string, unknown>;\n}\n\n/**\n * A node-supplied permission hint after strict SDK validation. This is\n * deliberately narrower than a general capability: no wildcards, caveats,\n * descriptions, or transport metadata can cross the service boundary.\n */\nexport interface PermissionHint {\n readonly service: \"tinycloud.kv\" | \"tinycloud.encryption\";\n readonly space?: string;\n readonly path: string;\n readonly actions: readonly string[];\n}\n\n/**\n * Storage quota information returned with quota-related errors.\n * @deprecated Read `usedBytes`, `limitBytes` and `account` from the error's\n * `meta`, or use `StorageFullEvent` from `tc.on(\"storage.full\", handler)`.\n */\nexport interface StorageQuotaInfo {\n usedBytes: number;\n limitBytes: number;\n service: string;\n}\n\n/**\n * Standard error codes used across services.\n */\nexport const ErrorCodes = {\n // Common errors\n NOT_FOUND: \"NOT_FOUND\",\n AUTH_EXPIRED: \"AUTH_EXPIRED\",\n AUTH_REQUIRED: \"AUTH_REQUIRED\",\n AUTH_UNAUTHORIZED: \"AUTH_UNAUTHORIZED\",\n /** HTTP 401: the delegation the invocation cites was revoked. */\n AUTH_DELEGATION_REVOKED: \"AUTH_DELEGATION_REVOKED\",\n /** HTTP 401: a delegation the cited one rests on was revoked. */\n AUTH_DELEGATION_ANCESTOR_REVOKED: \"AUTH_DELEGATION_ANCESTOR_REVOKED\",\n NETWORK_ERROR: \"NETWORK_ERROR\",\n NODE_NOT_RESPONDING: \"NODE_NOT_RESPONDING\",\n NODE_IDENTITY_MISMATCH: \"NODE_IDENTITY_MISMATCH\",\n PARTIAL_NODE_WRITE: \"PARTIAL_NODE_WRITE\",\n RESOURCE_NOT_ROUTED: \"RESOURCE_NOT_ROUTED\",\n TIMEOUT: \"TIMEOUT\",\n ABORTED: \"ABORTED\",\n INVALID_INPUT: \"INVALID_INPUT\",\n PERMISSION_DENIED: \"PERMISSION_DENIED\",\n\n // Node retry signals (tinycloud-node #347). `meta.retryable` says whether a\n // retry is safe; `meta.retryAfterMs` carries the node's Retry-After.\n /** HTTP 503 `sql_busy:<stage>` / `unavailable_retryable`/`read_audit_busy`: nothing was applied; retry after `meta.retryAfterMs`. */\n NODE_BUSY: \"NODE_BUSY\",\n /** HTTP 503 `request_timeout`: the node's deadline passed before any write began; nothing was applied. */\n REQUEST_TIMEOUT: \"REQUEST_TIMEOUT\",\n /** HTTP 500 `outcome_unknown`: a write may have been applied; read back before retrying a non-idempotent operation. */\n OUTCOME_UNKNOWN: \"OUTCOME_UNKNOWN\",\n\n // KV-specific errors\n KV_NOT_FOUND: \"KV_NOT_FOUND\",\n KV_WRITE_FAILED: \"KV_WRITE_FAILED\",\n KV_PRECONDITION_FAILED: \"KV_PRECONDITION_FAILED\",\n KV_CONFLICT: \"KV_CONFLICT\",\n KV_RESPONSE_TOO_LARGE: \"KV_RESPONSE_TOO_LARGE\",\n /** HTTP 410 from the change feed: discard sync state and restart without a cursor; `meta.reason` says why. */\n KV_SYNC_RESET_REQUIRED: \"KV_SYNC_RESET_REQUIRED\",\n /** HTTP 403 from the change feed: the retention grant was refused; `meta.reason` says why. */\n KV_RETENTION_GRANT_REFUSED: \"KV_RETENTION_GRANT_REFUSED\",\n\n // SQL-specific errors\n SQL_ERROR: \"SQL_ERROR\",\n SQL_PERMISSION_DENIED: \"SQL_PERMISSION_DENIED\",\n SQL_DATABASE_NOT_FOUND: \"SQL_DATABASE_NOT_FOUND\",\n SQL_RESPONSE_TOO_LARGE: \"SQL_RESPONSE_TOO_LARGE\",\n SQL_QUOTA_EXCEEDED: \"SQL_QUOTA_EXCEEDED\",\n SQL_INVALID_STATEMENT: \"SQL_INVALID_STATEMENT\",\n SQL_SCHEMA_ERROR: \"SQL_SCHEMA_ERROR\",\n SQL_READONLY_VIOLATION: \"SQL_READONLY_VIOLATION\",\n\n // Storage quota errors\n STORAGE_QUOTA_EXCEEDED: \"STORAGE_QUOTA_EXCEEDED\",\n STORAGE_LIMIT_REACHED: \"STORAGE_LIMIT_REACHED\",\n /** The node answered the usage read without usage numbers (it predates the read). */\n STORAGE_STATUS_UNAVAILABLE: \"STORAGE_STATUS_UNAVAILABLE\",\n\n // DuckDB-specific errors\n DUCKDB_ERROR: \"DUCKDB_ERROR\",\n DUCKDB_PERMISSION_DENIED: \"DUCKDB_PERMISSION_DENIED\",\n DUCKDB_DATABASE_NOT_FOUND: \"DUCKDB_DATABASE_NOT_FOUND\",\n DUCKDB_RESPONSE_TOO_LARGE: \"DUCKDB_RESPONSE_TOO_LARGE\",\n DUCKDB_QUOTA_EXCEEDED: \"DUCKDB_QUOTA_EXCEEDED\",\n DUCKDB_INVALID_STATEMENT: \"DUCKDB_INVALID_STATEMENT\",\n DUCKDB_SCHEMA_ERROR: \"DUCKDB_SCHEMA_ERROR\",\n DUCKDB_READONLY_VIOLATION: \"DUCKDB_READONLY_VIOLATION\",\n} as const;\n\nexport type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];\n\n// =============================================================================\n// Service Session\n// =============================================================================\n\n/**\n * Session data required for authenticated service operations.\n * Both TinyCloudSession and web-sdk Session can be cast to this interface.\n */\nexport interface ServiceSession {\n /** The delegation header containing the UCAN */\n delegationHeader: { Authorization: string };\n /** The delegation CID */\n delegationCid: string;\n /** The space ID for this session */\n spaceId: string;\n /** The verification method DID */\n verificationMethod: string;\n /** The session key JWK (required for invoke) */\n jwk: object;\n}\n\n// =============================================================================\n// Platform Dependencies (Injected)\n// =============================================================================\n\n/**\n * Headers type - compatible with both browser and Node.js.\n */\nexport type ServiceHeaders = Record<string, string> | [string, string][];\n\n/**\n * A single fact object to include in the UCAN invocation.\n * Facts are key-value objects that the server reads from the UCAN facts field.\n */\nexport interface InvocationFact {\n [key: string]: unknown;\n}\n\n/**\n * Facts to include in the UCAN invocation.\n * This is an array of fact objects per the UCAN spec.\n * Used to pass additional parameters that the server reads from the UCAN facts field.\n */\nexport type InvocationFacts = InvocationFact[];\n\n/**\n * Invoke function signature - platform-specific implementation injected via DI.\n * Both node-sdk-wasm and web-sdk-wasm export this with identical signature.\n *\n * @param session - The service session with delegation data\n * @param service - Service name (e.g., \"kv\")\n * @param path - Resource path or key\n * @param action - Action to perform (e.g., \"tinycloud.kv/get\")\n * @param facts - Optional facts to include in the UCAN (e.g., for capabilities/read params)\n * @returns Headers to include in the request\n */\nexport type InvokeFunction = (\n session: ServiceSession,\n service: string,\n path: string,\n action: string,\n facts?: InvocationFacts,\n) => ServiceHeaders;\n\n/**\n * Multi-resource invocation entry.\n */\nexport interface InvokeAnyEntry {\n /**\n * Legacy space-scoped resource. Optional when `resource` is provided.\n */\n spaceId?: string;\n service: string;\n path: string;\n action: string;\n /** Optional raw resource URI. When set, WASM signs this URI directly. */\n resource?: string;\n /** Capability caveats that must remain on this invocation attenuation. */\n caveats?: Record<string, unknown>[];\n}\n\n/**\n * Invoke function for minting a single authorization header that covers\n * multiple capabilities across one effective invoker.\n */\nexport type InvokeAnyFunction = (\n session: ServiceSession,\n entries: InvokeAnyEntry[],\n facts?: InvocationFacts,\n) => ServiceHeaders;\n\n/**\n * Fetch request options - compatible with standard fetch API.\n */\nexport interface FetchRequestInit {\n method?: string;\n headers?: ServiceHeaders;\n body?: Blob | FormData | string;\n signal?: AbortSignal;\n redirect?: \"error\";\n}\n\n/**\n * Fetch response interface - compatible with standard Response.\n */\nexport interface FetchResponse {\n ok: boolean;\n status: number;\n statusText: string;\n body?: unknown;\n headers: {\n get(name: string): string | null;\n };\n json(): Promise<unknown>;\n text(): Promise<string>;\n arrayBuffer(): Promise<ArrayBuffer>;\n blob(): Promise<Blob>;\n /** Present on standard Responses; the retry reads a clone so the caller's body stays intact. */\n clone?(): FetchResponse;\n /** Set by `ServiceContext.invokeWithRetry` on the response it returns: how many requests it took. */\n retry?: RetryStats;\n}\n\n/** What automatic retry cost one request, so latency can be split from backoff. */\nexport interface RetryStats {\n /** Requests sent, including the first (1 means no retry). */\n attempts: number;\n /** Total milliseconds spent waiting between attempts. */\n backoffMs: number;\n}\n\n/**\n * Fetch function signature - allows for custom fetch implementations.\n * Compatible with both browser fetch and Node.js fetch.\n */\nexport type FetchFunction = (\n url: string,\n init?: FetchRequestInit,\n) => Promise<FetchResponse>;\n\n// =============================================================================\n// Retry Policy\n// =============================================================================\n\n/**\n * Automatic retry of node responses that guarantee nothing was applied\n * (`NODE_BUSY`, `REQUEST_TIMEOUT`). Applied once, in the service context's\n * `invokeWithRetry`, which re-mints the signed invocation per attempt. Backoff\n * is full-jitter: each delay is uniform in `[0, step]`, where\n * `step` is the strategy's delay capped at `maxDelayMs`. A node `Retry-After`\n * replaces the delay. Opt out with `{ maxAttempts: 1 }`.\n */\nexport interface RetryPolicy {\n /** Maximum number of attempts (including initial) */\n maxAttempts: number;\n /** Backoff strategy between retries */\n backoff: \"none\" | \"linear\" | \"exponential\";\n /** Base delay in milliseconds for backoff calculation */\n baseDelayMs: number;\n /** Maximum delay in milliseconds between retries */\n maxDelayMs: number;\n /** Node error codes that trigger a retry. `OUTCOME_UNKNOWN` is never retried. */\n retryableErrors: string[];\n /** Total time budget for all attempts and waits, in milliseconds. Unset: no cap. */\n maxTotalMs?: number;\n}\n\n/**\n * Default retry policy.\n */\nexport const defaultRetryPolicy: RetryPolicy = {\n maxAttempts: 4,\n backoff: \"exponential\",\n baseDelayMs: 250,\n maxDelayMs: 4000,\n retryableErrors: [ErrorCodes.NODE_BUSY, ErrorCodes.REQUEST_TIMEOUT],\n maxTotalMs: 12000,\n};\n\n// =============================================================================\n// Service Context\n// =============================================================================\n\n/**\n * Event handler function type.\n */\nexport type EventHandler = (data: unknown) => void;\n\n/**\n * SDK telemetry event handler.\n */\nexport type TelemetryEventHandler = (event: string, data: unknown) => void;\n\n/**\n * Default-off telemetry configuration.\n */\nexport type TelemetryConfig =\n | boolean\n | {\n enabled?: boolean;\n onEvent?: TelemetryEventHandler;\n };\n\n/**\n * Service interface - base contract for all services.\n */\nexport interface IService {\n /** Initialize service with context */\n initialize(context: IServiceContext): void;\n\n /** Called when session changes (sign-in, sign-out, refresh) */\n onSessionChange(session: ServiceSession | null): void;\n\n /** Called when SDK signs out - should abort pending operations */\n onSignOut(): void;\n\n /** Service-specific configuration */\n readonly config: Record<string, unknown>;\n}\n\n/**\n * Context provided to services for accessing platform dependencies.\n * The SDK creates this context and passes it to services during initialization.\n */\nexport interface IServiceContext {\n // Session management\n /** Current active session, or null if not authenticated */\n readonly session: ServiceSession | null;\n /** Whether there is an active authenticated session */\n readonly isAuthenticated: boolean;\n\n // Platform dependencies (injected by SDK)\n /** Platform-specific invoke function from WASM binding */\n readonly invoke: InvokeFunction;\n /** Optional multi-resource invoke function */\n readonly invokeAny?: InvokeAnyFunction;\n /** Fetch function (defaults to globalThis.fetch) */\n readonly fetch: FetchFunction;\n /** Send a signed invocation, retrying a busy node; `build` re-signs per attempt. Absent: sends once. */\n invokeWithRetry?(\n url: string,\n build: () => FetchRequestInit | Promise<FetchRequestInit>,\n ): Promise<FetchResponse>;\n /** Available TinyCloud host URLs */\n readonly hosts: string[];\n\n // Cross-service access\n /** Get another registered service by name */\n getService<T extends IService>(name: string): T | undefined;\n\n // Telemetry/Events\n /** Emit a telemetry event */\n emit(event: string, data: unknown): void;\n /** Subscribe to events */\n on(event: string, handler: EventHandler): () => void;\n\n // Lifecycle\n /** Abort signal that fires when SDK signs out. */\n readonly abortSignal: AbortSignal;\n /** Additional abort signal for a scoped operation, when active. */\n readonly operationAbortSignal?: AbortSignal;\n /** Install an additional signal for the duration of a scoped operation. */\n setOperationAbortSignal?(signal: AbortSignal | undefined): void;\n // Retry policy\n /** Retry policy for failed requests */\n readonly retryPolicy: RetryPolicy;\n}\n\n// =============================================================================\n// Telemetry Events\n// =============================================================================\n\n/**\n * Event emitted before a service request.\n */\nexport interface ServiceRequestEvent {\n service: string;\n action: string;\n span?: string;\n key?: string;\n timestamp: number;\n}\n\n/**\n * Event emitted after a service response.\n */\nexport interface ServiceResponseEvent {\n service: string;\n action: string;\n span?: string;\n ok: boolean;\n duration: number;\n durationMs?: number;\n status?: number;\n}\n\n/**\n * Event emitted on service error.\n */\nexport interface ServiceErrorEvent {\n service: string;\n span?: string;\n error: ServiceError;\n}\n\n/**\n * Event emitted on retry attempt.\n */\nexport interface ServiceRetryEvent {\n service: string;\n attempt: number;\n maxAttempts: number;\n error: ServiceError;\n}\n\n/**\n * Generic named span event for aggregating operation timings.\n */\nexport interface TelemetrySpanEvent {\n span: string;\n ok: boolean;\n durationMs: number;\n service?: string;\n action?: string;\n status?: number;\n error?: ServiceError;\n}\n\n/**\n * Telemetry event names.\n */\nexport const TelemetryEvents = {\n SPAN: \"telemetry.span\",\n SERVICE_REQUEST: \"service.request\",\n SERVICE_RESPONSE: \"service.response\",\n SERVICE_ERROR: \"service.error\",\n SERVICE_RETRY: \"service.retry\",\n SESSION_CHANGED: \"session.changed\",\n SESSION_EXPIRED: \"session.expired\",\n REPLICATION_READ: \"replication.read\",\n REPLICATION_WRITE: \"replication.write\",\n REPLICATION_SYNC: \"replication.sync\",\n REPLICATION_STATE: \"replication.state\",\n REPLICATION_DIVERGENCE: \"replication.divergence\",\n REPLICATION_GRANT: \"replication.grant\",\n REPLICATION_LIFECYCLE: \"replication.lifecycle\",\n /** Emitted once, on the first write rejected because storage is full. */\n STORAGE_FULL: \"storage.full\",\n} as const;\n\n// =============================================================================\n// Helper Functions\n// =============================================================================\n\n/**\n * Create a success result.\n */\nexport function ok<T>(data: T): Result<T> {\n return { ok: true, data };\n}\n\n/**\n * Create an error result.\n */\nexport function err<E = ServiceError>(error: E): Result<never, E> {\n return { ok: false, error };\n}\n\n/**\n * Create a ServiceError.\n */\nexport function serviceError(\n code: string,\n message: string,\n service: string,\n options?: { cause?: Error; meta?: Record<string, unknown> },\n): ServiceError {\n return {\n code,\n message,\n service,\n cause: options?.cause,\n meta: options?.meta,\n };\n}\n","/**\n * Developer request log core. Events are sanitized by `buildRequestEvent`, the\n * only constructor, which copies an allowlist of fields. Values, headers,\n * bodies, tokens, delegations, and error text have no field to land in.\n */\n\nimport { TelemetryEvents, type IServiceContext } from \"../types\";\n\nexport type RequestService =\n | \"kv\"\n | \"sql\"\n | \"duckdb\"\n | \"hooks\"\n | \"vault\"\n | \"space\"\n | \"sync\"\n | \"other\";\n\nexport interface RequestEvent {\n v: 1;\n /** Monotonic per RequestLog. */\n id: number;\n /** Request start, ms since epoch. */\n at: number;\n service: RequestService;\n /** `service/action`, e.g. `kv/get`; `other` when unknown. */\n op: string;\n /** `first/` of the key (`[redacted]` when unsafe or slashless); full key only for allowlisted first segments. */\n path: string | null;\n /** Tail label of the space id, never the id. */\n space: string | null;\n /** URL host[:port] only. */\n host: string | null;\n /** Body bytes for writes, response bytes for reads; null when unknown. */\n bytes: number | null;\n /** Whole logical request, including retry backoff; null when unknown. */\n durationMs: number | null;\n attempts: number;\n ok: boolean;\n code: string;\n source: \"network\" | \"replica\";\n}\n\n/** A sanitized event before the log assigns its id. */\nexport type RequestDraft = Omit<RequestEvent, \"id\">;\n\nexport interface RequestLogReader {\n /** Copy, oldest first. */\n snapshot(): readonly RequestEvent[];\n subscribe(cb: (event: RequestEvent) => void): () => void;\n onClear(cb: () => void): () => void;\n}\n\n/**\n * The fetch layer's only view of the developer request log. A context without a\n * sink never links the sanitizer or the invocation-meta store.\n */\nexport interface RequestSink {\n record(event: RequestDraft): void;\n /** Derive and record one event from one transport attempt's raw facts. */\n observe(observation: FetchObservation): void;\n /** Attach service, path, and action to headers built for an invocation. */\n remember(headers: object, meta: InvocationMeta): void;\n}\n\n/** Transport facts for one attempt, read by `observeFetch`. */\nexport interface FetchObservation {\n url: string;\n requestHeaders: unknown;\n requestBody: unknown;\n responseContentLength: string | null | undefined;\n ok: boolean;\n code: string;\n durationMs: number;\n attempts: number;\n at: number;\n}\n\n/** Raw facts about one logical request. Only `buildRequestEvent` reads them. */\nexport interface RawRequest {\n service?: string;\n op?: string;\n /** Unscrubbed key path. */\n path?: string | null;\n /** Full space id; only its tail label is kept. */\n spaceId?: string | null;\n url?: string | null;\n bytes?: number | null;\n durationMs: number | null;\n attempts?: number;\n ok: boolean;\n code?: string;\n source?: string;\n at: number;\n}\n\nconst RING_SIZE = 500;\nconst MAX_PATH = 256;\nconst REDACTED = \"[redacted]\";\n\nconst SERVICE_NAME = /^(kv|sql|duckdb|hooks|vault|space|sync)$/;\nconst OP_SHAPE = /^[a-z]+\\/[A-Za-z]+$/;\nconst SPACE_LABEL = /^[a-z0-9][a-z0-9_-]{0,31}$/;\nconst CODE_SHAPE = /^(ok|HTTP_[1-5]\\d\\d|[A-Z][A-Z0-9_]{1,63}|ERROR)$/;\nconst PATH_LABEL = /^[a-z0-9][a-z0-9_-]{0,31}$/;\n/** First segments whose full key may be recorded. Empty: only `first/` is ever kept. */\nconst PATH_ALLOWLIST: ReadonlySet<string> = new Set<string>();\n\nfunction scrubPath(raw: string | null | undefined): string | null {\n if (typeof raw !== \"string\") return null;\n const bare = raw.split(/[?#]/, 1)[0] ?? \"\";\n if (bare === \"\") return null;\n let decoded: string;\n try {\n decoded = decodeURIComponent(bare);\n } catch {\n return REDACTED;\n }\n const stripped = decoded.replace(/^\\/+/, \"\").replace(/\\/+/g, \"/\");\n if (stripped === \"\") return null;\n const slash = stripped.indexOf(\"/\");\n // Slashless: the whole value may be a bare secret name.\n if (slash === -1) return REDACTED;\n const first = stripped.slice(0, slash).toLowerCase();\n if (!PATH_LABEL.test(first)) return REDACTED;\n if (PATH_ALLOWLIST.has(first)) return stripped.length > MAX_PATH ? stripped.slice(0, MAX_PATH) : stripped;\n return `${first}/`;\n}\n\nfunction spaceLabel(spaceId: string | null | undefined): string | null {\n if (typeof spaceId !== \"string\" || spaceId === \"\") return null;\n // Only the name after the last colon is kept; an id without a colon has no such name.\n const colon = spaceId.lastIndexOf(\":\");\n if (colon === -1) return \"space\";\n const tail = spaceId.slice(colon + 1);\n return SPACE_LABEL.test(tail) ? tail : \"space\";\n}\n\nfunction hostOf(url: string | null | undefined): string | null {\n if (typeof url !== \"string\") return null;\n try {\n return new URL(url).host || null;\n } catch {\n return null;\n }\n}\n\n/** UTF-8 byte length of a request body without consuming it; null when unknown. */\nexport function byteLengthOf(body: unknown): number | null {\n if (typeof body === \"string\") return new TextEncoder().encode(body).byteLength;\n if (typeof Blob !== \"undefined\" && body instanceof Blob) return body.size;\n if (body instanceof ArrayBuffer) return body.byteLength;\n if (ArrayBuffer.isView(body)) return body.byteLength;\n return null;\n}\n\n/** Response Content-Length as bytes; null unless the header is plain digits (blank, signed, or exponent forms are unknown). */\nexport function declaredLengthOf(header: string | null | undefined): number | null {\n if (typeof header !== \"string\") return null;\n const value = header.trim();\n return /^\\d+$/.test(value) ? Number(value) : null;\n}\n\n/** What the signing path knows about a request; read back by the fetch layer. */\nexport interface InvocationMeta {\n service: string;\n path: string;\n action: string;\n spaceId?: string | null;\n}\n\nconst invocationMeta = new WeakMap<object, InvocationMeta>();\n\nexport function invocationMetaOf(headers: unknown): InvocationMeta | undefined {\n return typeof headers === \"object\" && headers !== null ? invocationMeta.get(headers) : undefined;\n}\n\nexport function buildRequestEvent(raw: RawRequest): RequestDraft {\n const service: RequestService =\n raw.service !== undefined && SERVICE_NAME.test(raw.service)\n ? (raw.service as RequestService)\n : \"other\";\n const space = spaceLabel(raw.spaceId);\n const op = typeof raw.op === \"string\" && OP_SHAPE.test(raw.op) ? raw.op : \"other\";\n const code =\n raw.code === undefined ? (raw.ok ? \"ok\" : \"ERROR\") : CODE_SHAPE.test(raw.code) ? raw.code : \"ERROR\";\n const bytes =\n typeof raw.bytes === \"number\" && Number.isFinite(raw.bytes) && raw.bytes >= 0\n ? Math.floor(raw.bytes)\n : null;\n return {\n v: 1,\n at: raw.at,\n service,\n op,\n path: scrubPath(raw.path),\n space,\n host: hostOf(raw.url),\n bytes,\n durationMs: typeof raw.durationMs === \"number\" && Number.isFinite(raw.durationMs) ? Math.max(0, Math.round(raw.durationMs)) : null,\n attempts:\n raw.attempts !== undefined && Number.isFinite(raw.attempts) && raw.attempts >= 1\n ? Math.floor(raw.attempts)\n : 1,\n ok: raw.ok === true,\n code,\n source: raw.source === \"replica\" ? \"replica\" : \"network\",\n };\n}\n\n/**\n * Derives one event from one transport attempt. Invocation metadata comes from the\n * request headers. Bytes are the request body length when a body was sent (null for\n * streams); with no request body, the declared response length.\n */\nexport function observeFetch(observation: FetchObservation, emit: (event: RequestDraft) => void): void {\n const meta = invocationMetaOf(observation.requestHeaders);\n emit(\n buildRequestEvent({\n service: meta?.service ?? \"other\",\n op: meta ? `${meta.service}/${meta.action.split(\"/\").pop() ?? meta.action}` : \"other\",\n path: meta?.path ?? null,\n spaceId: meta?.spaceId ?? null,\n url: observation.url,\n bytes: requestBytesOf(observation.requestBody, observation.responseContentLength),\n durationMs: observation.durationMs,\n attempts: observation.attempts,\n ok: observation.ok,\n code: observation.code,\n at: observation.at,\n }),\n );\n}\n\n/**\n * Request bytes: the body length when a body was sent (null when it cannot be measured\n * without consuming it); with no body, the declared response Content-Length.\n */\nexport function requestBytesOf(body: unknown, responseContentLength: string | null | undefined): number | null {\n if (body === undefined || body === null) return declaredLengthOf(responseContentLength);\n return byteLengthOf(body);\n}\n\n/**\n * Maps replica-served KV reads (`replication.read` with `source: \"replica\"`) to\n * request events. Network reads and replication writes are not mapped here:\n * their wire requests are recorded by the fetch layer.\n */\nexport function observeReplicaReads(context: IServiceContext, sink: RequestSink): () => void {\n return context.on(TelemetryEvents.REPLICATION_READ, (data: unknown) => {\n try {\n const read = (typeof data === \"object\" && data !== null ? data : {}) as Record<string, unknown>;\n if (read.source !== \"replica\") return;\n sink.record(\n buildRequestEvent({\n service: \"kv\",\n op: `kv/${String(read.op)}`,\n path: typeof read.key === \"string\" ? read.key : null,\n spaceId: typeof read.space === \"string\" ? read.space : null,\n url: null,\n bytes: null,\n durationMs: typeof read.latencyMs === \"number\" ? read.latencyMs : null,\n attempts: 1,\n ok: read.outcome !== \"error\",\n code: read.outcome === \"not_found\" ? \"KV_NOT_FOUND\" : typeof read.code === \"string\" ? read.code : undefined,\n source: \"replica\",\n at: Date.now(),\n }),\n );\n } catch {\n // Observability must not affect KV behavior.\n }\n });\n}\n\ntype Listener<T extends unknown[]> = (...args: T) => void;\n\n/** Fixed-size in-memory ring of sanitized events. Lost on reload by design (v1). */\nexport class RequestLog implements RequestSink, RequestLogReader {\n readonly #slots: (RequestEvent | undefined)[] = new Array(RING_SIZE);\n #head = 0;\n #count = 0;\n #nextId = 1;\n readonly #subscribers = new Set<Listener<[RequestEvent]>>();\n readonly #clearListeners = new Set<Listener<[]>>();\n\n record(draft: RequestDraft): void {\n const event: RequestEvent = Object.freeze({ ...draft, id: this.#nextId++ });\n this.#slots[this.#head] = event;\n this.#head = (this.#head + 1) % RING_SIZE;\n if (this.#count < RING_SIZE) this.#count++;\n for (const cb of this.#subscribers) {\n try {\n cb(event);\n } catch {\n // A failing subscriber must not break the request path or other subscribers.\n }\n }\n }\n\n /** Called only when a sink is set, so developer-off code never writes here. */\n remember(headers: object, meta: InvocationMeta): void {\n invocationMeta.set(headers, meta);\n }\n\n observe(observation: FetchObservation): void {\n observeFetch(observation, (event) => this.record(event));\n }\n\n snapshot(): RequestEvent[] {\n const start = this.#count < RING_SIZE ? 0 : this.#head;\n const out: RequestEvent[] = [];\n for (let i = 0; i < this.#count; i++) {\n out.push(this.#slots[(start + i) % RING_SIZE]!);\n }\n return out;\n }\n\n subscribe(cb: (event: RequestEvent) => void): () => void {\n this.#subscribers.add(cb);\n return () => {\n this.#subscribers.delete(cb);\n };\n }\n\n onClear(cb: () => void): () => void {\n this.#clearListeners.add(cb);\n return () => {\n this.#clearListeners.delete(cb);\n };\n }\n\n clear(): void {\n this.#slots.fill(undefined);\n this.#head = 0;\n this.#count = 0;\n for (const cb of this.#clearListeners) {\n try {\n cb();\n } catch {\n // Isolated like subscribers.\n }\n }\n }\n}\n\n/** The public view of a log: reads and subscriptions only. `record` and `clear` stay with the owner. */\nexport function readOnlyRequestLog(log: RequestLog): RequestLogReader {\n return Object.freeze({\n snapshot: () => log.snapshot(),\n subscribe: (cb: (event: RequestEvent) => void) => log.subscribe(cb),\n onClear: (cb: () => void) => log.onClear(cb),\n });\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,4 +1,96 @@
|
|
|
1
|
-
import { P as PermissionHint } from './types-
|
|
1
|
+
import { F as FetchFunction, e as FetchResponse, S as ServiceSession, P as PermissionHint } from './types-DjAP8dve.cjs';
|
|
2
|
+
|
|
3
|
+
type ResourceAccess = "read" | "write" | "host" | "encryption";
|
|
4
|
+
interface ResourceDestination {
|
|
5
|
+
readonly url: string;
|
|
6
|
+
readonly nodeDid?: string;
|
|
7
|
+
}
|
|
8
|
+
interface ResourceNodeInfo {
|
|
9
|
+
readonly nodeId: string;
|
|
10
|
+
readonly features?: unknown;
|
|
11
|
+
}
|
|
12
|
+
interface ResourceRoutingConfig {
|
|
13
|
+
resolveResource(resource: string, access: ResourceAccess): Promise<readonly ResourceDestination[]>;
|
|
14
|
+
bindAudience(authorization: string, nodeDid: string): Promise<string>;
|
|
15
|
+
/** Defaults to the raw transport's public node identity checks. */
|
|
16
|
+
verifyDestination?(target: ResourceDestination): Promise<void | ResourceNodeInfo>;
|
|
17
|
+
}
|
|
18
|
+
/** An identity-checked destination bound to one live resource-routing decision. */
|
|
19
|
+
interface ResourceTransportTarget {
|
|
20
|
+
readonly url: string;
|
|
21
|
+
readonly nodeDid: string;
|
|
22
|
+
readonly fetch: FetchFunction;
|
|
23
|
+
readonly nodeInfo: ResourceNodeInfo;
|
|
24
|
+
bindAudience(authorization: string): Promise<string>;
|
|
25
|
+
}
|
|
26
|
+
interface ResourceTransportOptions {
|
|
27
|
+
/** CID-only controls are node-local and must not choose among several destinations. */
|
|
28
|
+
readonly nodeLocal?: boolean;
|
|
29
|
+
/** An existing node-bound authority must match the resolved destination. */
|
|
30
|
+
readonly nodeDid?: string;
|
|
31
|
+
/** A verified imported grant activates its resources independently, not atomically. */
|
|
32
|
+
readonly independentResources?: boolean;
|
|
33
|
+
/** Captured, verified share-session key for its own locally created invocations. */
|
|
34
|
+
readonly invocationSession?: ServiceSession;
|
|
35
|
+
}
|
|
36
|
+
type ResourceTransport = (resources: readonly {
|
|
37
|
+
resource: string;
|
|
38
|
+
access: ResourceAccess;
|
|
39
|
+
}[], send: (target: ResourceTransportTarget) => Promise<FetchResponse>, options?: ResourceTransportOptions) => Promise<FetchResponse>;
|
|
40
|
+
/** Compare both public node identity surfaces before sending a capability. */
|
|
41
|
+
declare function verifyResourceDestination(target: ResourceDestination, fetchFn: FetchFunction): Promise<ResourceNodeInfo | undefined>;
|
|
42
|
+
declare class NodeNotRespondingError extends Error {
|
|
43
|
+
readonly hosts: readonly string[];
|
|
44
|
+
readonly cause?: unknown | undefined;
|
|
45
|
+
readonly code = "NODE_NOT_RESPONDING";
|
|
46
|
+
constructor(hosts: readonly string[], cause?: unknown | undefined);
|
|
47
|
+
}
|
|
48
|
+
declare class NodeIdentityMismatchError extends Error {
|
|
49
|
+
readonly url: string;
|
|
50
|
+
readonly expectedDid: string;
|
|
51
|
+
readonly actualDid?: string | undefined;
|
|
52
|
+
readonly code = "NODE_IDENTITY_MISMATCH";
|
|
53
|
+
constructor(url: string, expectedDid: string, actualDid?: string | undefined);
|
|
54
|
+
}
|
|
55
|
+
declare class ResourceNotRoutedError extends Error {
|
|
56
|
+
readonly resource: string;
|
|
57
|
+
readonly code = "RESOURCE_NOT_ROUTED";
|
|
58
|
+
constructor(resource: string);
|
|
59
|
+
}
|
|
60
|
+
declare class PartialNodeWriteError extends Error {
|
|
61
|
+
readonly succeeded: readonly string[];
|
|
62
|
+
readonly failed: readonly {
|
|
63
|
+
url: string;
|
|
64
|
+
status?: number;
|
|
65
|
+
cause?: unknown;
|
|
66
|
+
}[];
|
|
67
|
+
readonly code = "PARTIAL_NODE_WRITE";
|
|
68
|
+
constructor(succeeded: readonly string[], failed: readonly {
|
|
69
|
+
url: string;
|
|
70
|
+
status?: number;
|
|
71
|
+
cause?: unknown;
|
|
72
|
+
}[]);
|
|
73
|
+
}
|
|
74
|
+
type RoutingFailureCode = "NODE_NOT_RESPONDING" | "DECRYPTION_DEPENDENCY_UNAVAILABLE" | "PARTIAL_NODE_WRITE" | "NODE_IDENTITY_MISMATCH" | "RESOURCE_NOT_ROUTED" | "registry_unavailable" | "node_not_found" | "node_registry_entry_exists";
|
|
75
|
+
interface RoutingFailure {
|
|
76
|
+
code: RoutingFailureCode;
|
|
77
|
+
message: string;
|
|
78
|
+
cause?: Error;
|
|
79
|
+
meta?: Record<string, unknown>;
|
|
80
|
+
}
|
|
81
|
+
/** Preserve routing failures through service-specific Result/error adapters. */
|
|
82
|
+
declare function routingFailure(error: unknown): RoutingFailure | undefined;
|
|
83
|
+
/** Extract the signed resource without inferring an account for node-local names. */
|
|
84
|
+
declare function routingResourceOf(uri: string): string;
|
|
85
|
+
/** Share target selection and read/write outcomes with resource-specific adapters. */
|
|
86
|
+
declare function routeResourceRequest(resources: readonly {
|
|
87
|
+
resource: string;
|
|
88
|
+
access: ResourceAccess;
|
|
89
|
+
}[], routing: Pick<ResourceRoutingConfig, "resolveResource">, send: (target: ResourceDestination) => Promise<FetchResponse>, options?: ResourceTransportOptions): Promise<FetchResponse>;
|
|
90
|
+
/** Route only locally generated data invocations through a captured share session. */
|
|
91
|
+
declare function createResourceTransportInvocationFetch(transport: ResourceTransport, session: ServiceSession): FetchFunction;
|
|
92
|
+
/** Route signed /invoke traffic. Non-invocation endpoints retain their own resource-specific adapters. */
|
|
93
|
+
declare function createResourceRoutingFetch(fetchFn: FetchFunction, routing: ResourceRoutingConfig): FetchFunction;
|
|
2
94
|
|
|
3
95
|
/**
|
|
4
96
|
* Canonical JSON serialization and content hashing for TinyCloud
|
|
@@ -41,6 +133,40 @@ declare function utf8Decode(b: Uint8Array): string;
|
|
|
41
133
|
*/
|
|
42
134
|
declare function canonicalHashHex(sha256: (bytes: Uint8Array) => Uint8Array, value: Json): string;
|
|
43
135
|
|
|
136
|
+
declare const KEY_WRAP_V2_ALG: "x25519-hkdf-aes256gcm/v2";
|
|
137
|
+
interface KeyWrapContext {
|
|
138
|
+
purpose: "network-key" | "receiver-key";
|
|
139
|
+
networkId: string;
|
|
140
|
+
keyPath: string;
|
|
141
|
+
keyVersion: number;
|
|
142
|
+
metadata: Record<string, string>;
|
|
143
|
+
}
|
|
144
|
+
interface KeyWrapV2Header extends KeyWrapContext {
|
|
145
|
+
v: 2;
|
|
146
|
+
alg: typeof KEY_WRAP_V2_ALG;
|
|
147
|
+
kdf: "hkdf-sha256";
|
|
148
|
+
aead: "aes-256-gcm";
|
|
149
|
+
salt: string;
|
|
150
|
+
ephemeralPublicKey: string;
|
|
151
|
+
recipientPublicKey: string;
|
|
152
|
+
}
|
|
153
|
+
declare function isKeyWrapV2(wrapped: Uint8Array): boolean;
|
|
154
|
+
/** Parse the closed canonical header. Authentication occurs when the node or recipient opens it. */
|
|
155
|
+
declare function readKeyWrapV2(wrapped: Uint8Array): {
|
|
156
|
+
header: KeyWrapV2Header;
|
|
157
|
+
aad: Uint8Array;
|
|
158
|
+
nonce: Uint8Array;
|
|
159
|
+
ciphertext: Uint8Array;
|
|
160
|
+
};
|
|
161
|
+
declare function assertKeyWrapContext(header: KeyWrapV2Header, expected: KeyWrapContext): void;
|
|
162
|
+
declare function sealKeyV2(recipientPublicKey: Uint8Array, plaintext: Uint8Array, context: KeyWrapContext): Uint8Array;
|
|
163
|
+
declare function openKeyV2(recipientPrivateKey: Uint8Array, wrapped: Uint8Array, expected: KeyWrapContext): Uint8Array;
|
|
164
|
+
/** Match the declared algorithm without falling back after a TCW2 marker. */
|
|
165
|
+
declare function assertKeyWrapAlgorithm(wrapped: Uint8Array, alg: string, requireV2?: boolean): void;
|
|
166
|
+
type ColumnFormat = "encrypted" | "legacy-plaintext";
|
|
167
|
+
/** Stored format, not input bytes, determines whether a column is plaintext. */
|
|
168
|
+
declare function decryptColumn(decrypt: (key: Uint8Array, ciphertext: Uint8Array) => Uint8Array, key: Uint8Array, blob: Uint8Array, expectedFormat: ColumnFormat, strictEncryption?: boolean): Uint8Array;
|
|
169
|
+
|
|
44
170
|
/**
|
|
45
171
|
* Type definitions for TinyCloud one-of-one encryption.
|
|
46
172
|
*
|
|
@@ -95,7 +221,7 @@ interface InlineEncryptedEnvelope {
|
|
|
95
221
|
ciphertext: string;
|
|
96
222
|
/** Base64-encoded associated data, if any. */
|
|
97
223
|
aad?: string;
|
|
98
|
-
/** Caller
|
|
224
|
+
/** Caller metadata; v2 wraps bind it to the node-authenticated header. */
|
|
99
225
|
metadata?: Record<string, string>;
|
|
100
226
|
}
|
|
101
227
|
/**
|
|
@@ -216,13 +342,16 @@ interface EncryptionCrypto {
|
|
|
216
342
|
* a sealed-box / hpke / x25519+symmetric construction. Implementation
|
|
217
343
|
* is opaque; only the node can unwrap.
|
|
218
344
|
*/
|
|
219
|
-
sealToNetworkKey(networkPublicKey: Uint8Array, symmetricKey: Uint8Array
|
|
345
|
+
sealToNetworkKey(networkPublicKey: Uint8Array, symmetricKey: Uint8Array, options?: {
|
|
346
|
+
keyWrapV2: boolean;
|
|
347
|
+
context: KeyWrapContext;
|
|
348
|
+
}): Uint8Array;
|
|
220
349
|
/**
|
|
221
350
|
* Open a wrapped symmetric key that was re-encrypted to the
|
|
222
351
|
* per-request receiver public key. The matching private key must be
|
|
223
352
|
* supplied here; the SDK never sends it to the node.
|
|
224
353
|
*/
|
|
225
|
-
openWithReceiverKey(receiverPrivateKey: Uint8Array, wrappedKey: Uint8Array): Uint8Array;
|
|
354
|
+
openWithReceiverKey(receiverPrivateKey: Uint8Array, wrappedKey: Uint8Array, expected?: KeyWrapContext): Uint8Array;
|
|
226
355
|
/**
|
|
227
356
|
* Verify an ed25519 signature over `message` produced by the node
|
|
228
357
|
* identified by `nodeId` (the public-key DID).
|
|
@@ -285,7 +414,7 @@ interface DecryptInvocationSigner {
|
|
|
285
414
|
/**
|
|
286
415
|
* Errors thrown / returned from the encryption module.
|
|
287
416
|
*/
|
|
288
|
-
type EncryptionErrorInput = {
|
|
417
|
+
type EncryptionErrorInput = RoutingFailure | {
|
|
289
418
|
code: "NETWORK_NOT_FOUND";
|
|
290
419
|
networkId?: string;
|
|
291
420
|
name?: string;
|
|
@@ -330,4 +459,4 @@ declare function encryptionError(input: EncryptionErrorInput): EncryptionError;
|
|
|
330
459
|
/** Re-export for ergonomic typing of canonical payloads. */
|
|
331
460
|
type CanonicalJson = Json;
|
|
332
461
|
|
|
333
|
-
export { type BuildDecryptInvocationInput as B, type CanonicalJson as C, type DecryptCapabilityProof as D, type EncryptionCrypto as E, type InlineEncryptedEnvelope as I, type Json as J, type NetworkDescriptor as N, type
|
|
462
|
+
export { KEY_WRAP_V2_ALG as $, type ResourceTransportTarget as A, type BuildDecryptInvocationInput as B, type CanonicalJson as C, type DecryptCapabilityProof as D, type EncryptionCrypto as E, base64Decode as F, base64Encode as G, canonicalHashHex as H, type InlineEncryptedEnvelope as I, type Json as J, canonicalize as K, createResourceRoutingFetch as L, createResourceTransportInvocationFetch as M, type NetworkDescriptor as N, encryptionError as O, PartialNodeWriteError as P, hexDecode as Q, type RoutingFailure as R, hexEncode as S, routeResourceRequest as T, routingFailure as U, routingResourceOf as V, utf8Decode as W, utf8Encode as X, verifyResourceDestination as Y, type KeyWrapContext as Z, type ColumnFormat as _, type BuiltDecryptInvocation as a, type KeyWrapV2Header as a0, assertKeyWrapAlgorithm as a1, assertKeyWrapContext as a2, decryptColumn as a3, isKeyWrapV2 as a4, openKeyV2 as a5, readKeyWrapV2 as a6, sealKeyV2 as a7, DECRYPT_ACTION as b, DECRYPT_FACT_TYPE as c, DECRYPT_RESULT_TYPE as d, DEFAULT_ENCRYPTION_ALG as e, DEFAULT_KEY_VERSION as f, type DecryptInvocationFact as g, type DecryptInvocationSigner as h, type DecryptRequestBody as i, type DecryptResponseBody as j, ENCRYPTION_SERVICE as k, ENCRYPTION_SERVICE_SHORT as l, ENVELOPE_VERSION as m, type EncryptionError as n, type EncryptionErrorInput as o, NodeIdentityMismatchError as p, NodeNotRespondingError as q, type ReceiverKeyPair as r, type ReceiverKeySigner as s, type ResourceAccess as t, type ResourceDestination as u, type ResourceNodeInfo as v, ResourceNotRoutedError as w, type ResourceRoutingConfig as x, type ResourceTransport as y, type ResourceTransportOptions as z };
|
|
@@ -77,6 +77,10 @@ declare const ErrorCodes: {
|
|
|
77
77
|
/** HTTP 401: a delegation the cited one rests on was revoked. */
|
|
78
78
|
readonly AUTH_DELEGATION_ANCESTOR_REVOKED: "AUTH_DELEGATION_ANCESTOR_REVOKED";
|
|
79
79
|
readonly NETWORK_ERROR: "NETWORK_ERROR";
|
|
80
|
+
readonly NODE_NOT_RESPONDING: "NODE_NOT_RESPONDING";
|
|
81
|
+
readonly NODE_IDENTITY_MISMATCH: "NODE_IDENTITY_MISMATCH";
|
|
82
|
+
readonly PARTIAL_NODE_WRITE: "PARTIAL_NODE_WRITE";
|
|
83
|
+
readonly RESOURCE_NOT_ROUTED: "RESOURCE_NOT_ROUTED";
|
|
80
84
|
readonly TIMEOUT: "TIMEOUT";
|
|
81
85
|
readonly ABORTED: "ABORTED";
|
|
82
86
|
readonly INVALID_INPUT: "INVALID_INPUT";
|
|
@@ -77,6 +77,10 @@ declare const ErrorCodes: {
|
|
|
77
77
|
/** HTTP 401: a delegation the cited one rests on was revoked. */
|
|
78
78
|
readonly AUTH_DELEGATION_ANCESTOR_REVOKED: "AUTH_DELEGATION_ANCESTOR_REVOKED";
|
|
79
79
|
readonly NETWORK_ERROR: "NETWORK_ERROR";
|
|
80
|
+
readonly NODE_NOT_RESPONDING: "NODE_NOT_RESPONDING";
|
|
81
|
+
readonly NODE_IDENTITY_MISMATCH: "NODE_IDENTITY_MISMATCH";
|
|
82
|
+
readonly PARTIAL_NODE_WRITE: "PARTIAL_NODE_WRITE";
|
|
83
|
+
readonly RESOURCE_NOT_ROUTED: "RESOURCE_NOT_ROUTED";
|
|
80
84
|
readonly TIMEOUT: "TIMEOUT";
|
|
81
85
|
readonly ABORTED: "ABORTED";
|
|
82
86
|
readonly INVALID_INPUT: "INVALID_INPUT";
|