@tinycloud/server 2.4.13-beta.5 → 2.5.0-beta.11
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/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -152,7 +152,7 @@ async function readDelegatedSecret(node, delegation, name, options) {
|
|
|
152
152
|
const result = await access.kv.get(secretKey, { raw: true, prefix: "" });
|
|
153
153
|
if (!result.ok) {
|
|
154
154
|
const message = result.error?.message ?? `failed to read ${secretKey}`;
|
|
155
|
-
throw new Error(`delegated secret ${name} KV get failed: ${message}`);
|
|
155
|
+
throw Object.assign(new Error(`delegated secret ${name} KV get failed: ${message}`), { cause: result.error });
|
|
156
156
|
}
|
|
157
157
|
const envelope = parseEncryptedEnvelope(
|
|
158
158
|
result.data?.data,
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/identity.ts","../src/delegated-secrets.ts","../src/siwe-session.ts"],"sourcesContent":["export {\n createServerIdentity,\n deriveDstackPrivateKey,\n isTinyCloudSessionError,\n serverDidForPrivateKey,\n withSessionRefresh,\n type CreateServerIdentityOptions,\n type DeriveDstackPrivateKeyOptions,\n type DstackKeyClient,\n type ServerIdentity,\n} from \"./identity.js\";\n\nexport {\n createServerDelegateClient,\n parseDelegation,\n parseEncryptedEnvelope,\n parseSecretPayload,\n readDelegatedSecret,\n type CreateServerDelegateClientOptions,\n type DelegationInput,\n type ServerDelegateClient,\n} from \"./delegated-secrets.js\";\n\nexport {\n NonceStore,\n ServerAuthError,\n createSiweSession,\n issueSessionToken,\n verifySessionToken,\n verifySiweMessage,\n type CreateSiweSessionOptions,\n type SessionClaims,\n type SessionToken,\n type VerifiedSiwe,\n} from \"./siwe-session.js\";\n","import { keccak256, type Hex } from \"viem\";\nimport { privateKeyToAccount } from \"viem/accounts\";\nimport { TinyCloudNode, type Manifest, type TinyCloudNodeConfig } from \"@tinycloud/node-sdk\";\nimport { authorizationVerdictOf } from \"@tinycloud/sdk-core\";\n\nconst DEFAULT_HOST = \"https://node.tinycloud.xyz\";\n\nexport interface DstackKeyClient {\n getKey(path: string, purpose: string): Promise<{ key: Uint8Array }>;\n}\n\nexport interface DeriveDstackPrivateKeyOptions {\n client: DstackKeyClient;\n path: string;\n purpose: string;\n}\n\nexport async function deriveDstackPrivateKey(\n options: DeriveDstackPrivateKeyOptions,\n): Promise<Hex> {\n const res = await options.client.getKey(options.path, options.purpose);\n if (!(res.key instanceof Uint8Array) || res.key.length === 0) {\n throw new Error(\"dstack getKey returned no key material\");\n }\n return keccak256(res.key);\n}\n\nexport function serverDidForPrivateKey(privateKey: string): string {\n const account = privateKeyToAccount(privateKey as Hex);\n return `did:pkh:eip155:1:${account.address}`;\n}\n\nexport interface CreateServerIdentityOptions {\n privateKey: string;\n host?: string;\n prefix?: string;\n manifest?: Manifest | Manifest[];\n autoCreateSpace?: boolean;\n enablePublicSpace?: boolean;\n includeAccountRegistryPermissions?: boolean;\n nodeConfig?: Omit<\n TinyCloudNodeConfig,\n | \"privateKey\"\n | \"host\"\n | \"prefix\"\n | \"manifest\"\n | \"autoCreateSpace\"\n | \"enablePublicSpace\"\n | \"includeAccountRegistryPermissions\"\n >;\n}\n\nexport interface ServerIdentity {\n node: TinyCloudNode;\n did: string;\n host: string;\n privateKey: string;\n}\n\nexport async function createServerIdentity(\n options: CreateServerIdentityOptions,\n): Promise<ServerIdentity> {\n const host = options.host ?? DEFAULT_HOST;\n const node = new TinyCloudNode({\n ...options.nodeConfig,\n privateKey: options.privateKey,\n host,\n prefix: options.prefix,\n manifest: options.manifest,\n autoCreateSpace: options.autoCreateSpace ?? false,\n enablePublicSpace: options.enablePublicSpace ?? false,\n includeAccountRegistryPermissions: options.includeAccountRegistryPermissions ?? false,\n });\n\n await node.signIn();\n\n return {\n node,\n did: node.did,\n host,\n privateKey: options.privateKey,\n };\n}\n\n// Message heuristics, for untyped errors only.\n//\n// 1. Double-quoted strings are dropped (escape-aware). The SDK writes\n// caller-chosen names such as KV keys with `JSON.stringify`, so a status-like\n// number or quote inside a key can never be read as, or hide, the status.\n// 2. The first number in 400-599 in one of the SDK's diagnostic positions is\n// the status:\n// - `: NNN` followed by whitespace or the end — KV `…key \"k\": 403 - text`,\n// hooks `…webhook: 401 hook ticket expired`, `…with server: 403`,\n// `…failed: 401`\n// - `HTTP NNN`, `returned NNN`, `rejected (NNN)`\n// - a trailing `(NNN)` or `(NNN).`\n// Only a 401 refreshes, so a real 502 or 403 is never overridden by a `401`\n// later in its body.\n// 3. With no status, session wording on the unstripped message decides.\nconst QUOTED_STRING_PATTERN = /\"(?:[^\"\\\\]|\\\\.)*\"/g;\nconst DIAGNOSTIC_STATUS_PATTERN =\n /:\\s(\\d{3})(?=\\s|$)|\\bHTTP\\s(\\d{3})\\b|\\breturned\\s(\\d{3})\\b|\\brejected\\s\\((\\d{3})\\)|\\((\\d{3})\\)\\.?$/gi;\nconst SESSION_ERROR_PATTERN =\n /\\b(session\\s+expired|invalid\\s+session|token\\s+expired|expired\\s+credentials?|unauthorized|unauthenticated|sign.?in\\s*required)\\b/i;\n\n/** The first HTTP error status in a diagnostic position, ignoring quoted strings. */\nfunction diagnosticStatusOf(message: string): number | undefined {\n const unquoted = message.replace(QUOTED_STRING_PATTERN, '\"\"');\n for (const match of unquoted.matchAll(DIAGNOSTIC_STATUS_PATTERN)) {\n const status = Number(match.slice(1).find((group) => group !== undefined));\n if (status >= 400 && status <= 599) return status;\n }\n return undefined;\n}\n\n/**\n * Whether re-signing in can fix `error`. Typed errors (HTTP `status`,\n * `statusCode` or `meta.status`, or `AUTH_UNAUTHORIZED`, including through the\n * `cause` chain) decide from their structure: only a 401 refreshes, so a 403\n * never does whatever its body says. Untyped errors fall back to the message.\n */\nexport function isTinyCloudSessionError(error: unknown): boolean {\n const verdict = authorizationVerdictOf(error);\n if (verdict !== undefined) {\n return verdict === \"unauthenticated\";\n }\n const message = error instanceof Error ? error.message : String(error);\n const status = diagnosticStatusOf(message);\n if (status !== undefined) {\n return status === 401;\n }\n return SESSION_ERROR_PATTERN.test(message);\n}\n\n/**\n * Run `fn`, re-signing in and retrying once when it fails with a session\n * error (see {@link isTinyCloudSessionError}). To keep the typed status when\n * unwrapping a `Result`, throw the `ServiceError` itself or an `Error` whose\n * `cause` is the `ServiceError`.\n */\nexport async function withSessionRefresh<T>(\n node: TinyCloudNode,\n fn: () => Promise<T>,\n): Promise<T> {\n try {\n return await fn();\n } catch (error) {\n if (isTinyCloudSessionError(error)) {\n await node.signIn();\n return fn();\n }\n throw error;\n }\n}\n","import {\n TinyCloudNode,\n deserializeDelegation,\n type InlineEncryptedEnvelope,\n type PortableDelegation,\n type SecretScopeOptions,\n} from \"@tinycloud/node-sdk\";\nimport { resolveSecretPath } from \"@tinycloud/sdk-core\";\nimport type { CreateServerIdentityOptions } from \"./identity.js\";\nimport { createServerIdentity } from \"./identity.js\";\n\ntype KvLike = {\n get<T = unknown>(\n key: string,\n options: { raw: true; prefix: string },\n ): Promise<{ ok: true; data: T | unknown } | { ok: false; error?: { message?: string } }>;\n};\n\ntype DelegatedAccessLike = {\n kv: KvLike;\n delegation: Pick<PortableDelegation, \"cid\">;\n restorable?: { delegationCid?: string };\n};\n\ntype DecryptResult =\n | { ok: true; data: Uint8Array }\n | { ok: false; error: { code?: string; message: string } };\n\ntype EncryptionLike = {\n decryptEnvelope(\n envelope: InlineEncryptedEnvelope,\n capabilityProof: { proofs: string[] },\n ): Promise<DecryptResult>;\n};\n\ntype TinyCloudNodeLike = {\n signIn(): Promise<unknown>;\n useDelegation(delegation: PortableDelegation): Promise<DelegatedAccessLike>;\n encryption: EncryptionLike;\n};\n\nexport type DelegationInput = PortableDelegation | string;\n\nexport interface ServerDelegateClient {\n getSecret(name: string, options?: SecretScopeOptions): Promise<string>;\n}\n\nexport interface CreateServerDelegateClientOptions {\n privateKey: string;\n host?: string;\n delegation: DelegationInput;\n prefix?: string;\n nodeConfig?: CreateServerIdentityOptions[\"nodeConfig\"];\n node?: TinyCloudNodeLike;\n nodeFactory?: (options: CreateServerIdentityOptions) => Promise<TinyCloudNodeLike>;\n}\n\nexport function createServerDelegateClient(\n options: CreateServerDelegateClientOptions,\n): ServerDelegateClient {\n const delegation = parseDelegation(options.delegation);\n let nodePromise: Promise<TinyCloudNodeLike> | undefined;\n\n async function getNode(): Promise<TinyCloudNodeLike> {\n if (!nodePromise) {\n if (options.node) {\n nodePromise = Promise.resolve(options.node);\n return options.node;\n }\n const identityOptions: CreateServerIdentityOptions = {\n privateKey: options.privateKey,\n host: options.host,\n prefix: options.prefix,\n enablePublicSpace: false,\n includeAccountRegistryPermissions: false,\n nodeConfig: options.nodeConfig,\n };\n const created = options.nodeFactory\n ? options.nodeFactory(identityOptions)\n : createServerIdentity(identityOptions).then(\n (identity) => identity.node as unknown as TinyCloudNodeLike,\n );\n nodePromise = created;\n return created;\n }\n return nodePromise;\n }\n\n return {\n async getSecret(name: string, secretOptions?: SecretScopeOptions): Promise<string> {\n const node = await getNode();\n return readDelegatedSecret(node, delegation, name, secretOptions);\n },\n };\n}\n\nexport async function readDelegatedSecret(\n node: TinyCloudNodeLike | TinyCloudNode,\n delegation: PortableDelegation,\n name: string,\n options?: SecretScopeOptions,\n): Promise<string> {\n const secretKey = resolveSecretPath(name, options).permissionPaths.vault;\n\n const access = await node.useDelegation(delegation);\n const result = await access.kv.get<unknown>(secretKey, { raw: true, prefix: \"\" });\n if (!result.ok) {\n const message = result.error?.message ?? `failed to read ${secretKey}`;\n throw new Error(`delegated secret ${name} KV get failed: ${message}`);\n }\n\n const envelope = parseEncryptedEnvelope(\n (result.data as { data?: unknown } | undefined)?.data,\n name,\n );\n const proofCid = access.restorable?.delegationCid ?? access.delegation.cid;\n if (!proofCid) {\n throw new Error(`delegated secret ${name} has no decrypt proof`);\n }\n\n const decrypted = await node.encryption.decryptEnvelope(envelope, { proofs: [proofCid] });\n if (!decrypted.ok) {\n throw new Error(`delegated secret ${name} decrypt failed: ${decrypted.error.message}`);\n }\n\n return parseSecretPayload(decrypted.data, name);\n}\n\nexport function parseDelegation(delegation: DelegationInput): PortableDelegation {\n return typeof delegation === \"string\" ? deserializeDelegation(delegation) : delegation;\n}\n\nexport function parseEncryptedEnvelope(\n rawEnvelope: unknown,\n name = \"secret\",\n): InlineEncryptedEnvelope {\n const parsed = typeof rawEnvelope === \"string\" ? JSON.parse(rawEnvelope) : rawEnvelope;\n if (\n typeof parsed !== \"object\" ||\n parsed === null ||\n typeof (parsed as Partial<InlineEncryptedEnvelope>).v !== \"number\" ||\n typeof (parsed as Partial<InlineEncryptedEnvelope>).networkId !== \"string\" ||\n typeof (parsed as Partial<InlineEncryptedEnvelope>).ciphertext !== \"string\" ||\n typeof (parsed as Partial<InlineEncryptedEnvelope>).encryptedSymmetricKey !== \"string\"\n ) {\n throw new Error(`delegated secret ${name} did not contain an encrypted envelope`);\n }\n return parsed as InlineEncryptedEnvelope;\n}\n\nexport function parseSecretPayload(plaintext: Uint8Array, name = \"secret\"): string {\n let parsed: { value?: unknown };\n try {\n parsed = JSON.parse(new TextDecoder().decode(plaintext)) as { value?: unknown };\n } catch {\n throw new Error(`delegated secret ${name} did not contain valid JSON`);\n }\n if (typeof parsed.value !== \"string\") {\n throw new Error(`delegated secret ${name} did not contain a string value`);\n }\n return parsed.value;\n}\n","import { createHmac, randomBytes, timingSafeEqual } from \"node:crypto\";\nimport { getAddress, recoverMessageAddress, type Hex } from \"viem\";\n\nconst DEFAULT_NONCE_TTL_MS = 5 * 60 * 1000;\nconst DEFAULT_SESSION_TTL_SECONDS = 24 * 60 * 60;\n\ninterface NonceEntry {\n address: string;\n createdAt: number;\n}\n\nexport class ServerAuthError extends Error {}\n\nexport class NonceStore {\n private readonly nonces = new Map<string, NonceEntry>();\n\n constructor(private readonly ttlMs = DEFAULT_NONCE_TTL_MS) {}\n\n issue(address: string): string {\n this.sweep();\n const normalized = getAddress(address).toLowerCase();\n const nonce = randomBytes(16).toString(\"hex\");\n this.nonces.set(this.key(normalized, nonce), {\n address: normalized,\n createdAt: Date.now(),\n });\n return nonce;\n }\n\n validate(address: string, nonce: string): boolean {\n const normalized = getAddress(address).toLowerCase();\n const key = this.key(normalized, nonce);\n const entry = this.nonces.get(key);\n if (!entry) return false;\n this.nonces.delete(key);\n return Date.now() - entry.createdAt <= this.ttlMs;\n }\n\n private key(address: string, nonce: string): string {\n return `${address}:${nonce}`;\n }\n\n private sweep(): void {\n const now = Date.now();\n for (const [key, entry] of this.nonces) {\n if (now - entry.createdAt > this.ttlMs) this.nonces.delete(key);\n }\n }\n}\n\nexport interface VerifiedSiwe {\n address: string;\n nonce: string;\n}\n\nexport async function verifySiweMessage(\n message: string,\n signature: string,\n): Promise<VerifiedSiwe> {\n const lines = message.split(\"\\n\");\n let claimed: string;\n try {\n claimed = getAddress((lines[1] ?? \"\").trim());\n } catch {\n throw new ServerAuthError(\"SIWE message is missing a valid address on line 2\");\n }\n\n const nonceMatch = message.match(/^Nonce: (.+)$/m);\n if (!nonceMatch || nonceMatch[1] === undefined) {\n throw new ServerAuthError(\"SIWE message is missing a Nonce line\");\n }\n const nonce = nonceMatch[1].trim();\n\n let recovered: string;\n try {\n recovered = await recoverMessageAddress({ message, signature: signature as Hex });\n } catch (error) {\n throw new ServerAuthError(\n `SIWE signature recovery failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n if (getAddress(recovered) !== claimed) {\n throw new ServerAuthError(\n `SIWE signature does not match message address (recovered ${recovered}, expected ${claimed})`,\n );\n }\n\n return { address: claimed, nonce };\n}\n\nexport interface SessionToken {\n token: string;\n expiresIn: number;\n}\n\nexport interface SessionClaims {\n sub: string;\n address: string;\n iat: number;\n exp: number;\n}\n\nexport function issueSessionToken(\n address: string,\n secret: string,\n ttlSeconds = DEFAULT_SESSION_TTL_SECONDS,\n): SessionToken {\n const normalized = getAddress(address);\n const now = Math.floor(Date.now() / 1000);\n const claims: SessionClaims = {\n sub: normalized,\n address: normalized,\n iat: now,\n exp: now + ttlSeconds,\n };\n return {\n token: signJwt({ alg: \"HS256\", typ: \"JWT\" }, claims, secret),\n expiresIn: ttlSeconds,\n };\n}\n\nexport function verifySessionToken(token: string, secret: string): { address: string } {\n const claims = verifyJwt(token, secret);\n if (typeof claims.sub !== \"string\" || claims.sub === \"\") {\n throw new ServerAuthError(\"session token missing 'sub' claim\");\n }\n return { address: claims.sub };\n}\n\nexport interface CreateSiweSessionOptions {\n jwtSecret: string;\n nonceStore?: NonceStore;\n sessionTtlSeconds?: number;\n}\n\nexport function createSiweSession(options: CreateSiweSessionOptions) {\n const nonceStore = options.nonceStore ?? new NonceStore();\n const sessionTtlSeconds = options.sessionTtlSeconds ?? DEFAULT_SESSION_TTL_SECONDS;\n\n return {\n issueNonce(address: string): string {\n return nonceStore.issue(address);\n },\n async verify(message: string, signature: string): Promise<SessionToken> {\n const verified = await verifySiweMessage(message, signature);\n if (!nonceStore.validate(verified.address, verified.nonce)) {\n throw new ServerAuthError(\"nonce is invalid, expired, or already used\");\n }\n return issueSessionToken(verified.address, options.jwtSecret, sessionTtlSeconds);\n },\n verifyToken(token: string): { address: string } {\n return verifySessionToken(token, options.jwtSecret);\n },\n };\n}\n\nfunction signJwt(header: object, payload: object, secret: string): string {\n const encodedHeader = base64UrlEncode(JSON.stringify(header));\n const encodedPayload = base64UrlEncode(JSON.stringify(payload));\n const signingInput = `${encodedHeader}.${encodedPayload}`;\n const signature = createHmac(\"sha256\", secret).update(signingInput).digest();\n return `${signingInput}.${base64UrlEncode(signature)}`;\n}\n\nfunction verifyJwt(token: string, secret: string): SessionClaims {\n const parts = token.split(\".\");\n if (parts.length !== 3 || !parts[0] || !parts[1] || !parts[2]) {\n throw new ServerAuthError(\"session token is not a valid JWT\");\n }\n\n const signingInput = `${parts[0]}.${parts[1]}`;\n const expected = createHmac(\"sha256\", secret).update(signingInput).digest();\n const actual = base64UrlDecode(parts[2]);\n if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) {\n throw new ServerAuthError(\"session token signature verification failed\");\n }\n\n const claims = JSON.parse(base64UrlDecode(parts[1]).toString(\"utf8\")) as SessionClaims;\n if (typeof claims.exp !== \"number\" || claims.exp <= Math.floor(Date.now() / 1000)) {\n throw new ServerAuthError(\"session token expired\");\n }\n return claims;\n}\n\nfunction base64UrlEncode(value: string | Buffer): string {\n const buffer = typeof value === \"string\" ? Buffer.from(value, \"utf8\") : value;\n return buffer.toString(\"base64url\");\n}\n\nfunction base64UrlDecode(value: string): Buffer {\n return Buffer.from(value, \"base64url\");\n}\n"],"mappings":"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","names":["import_node_sdk","import_sdk_core","import_viem"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/identity.ts","../src/delegated-secrets.ts","../src/siwe-session.ts"],"sourcesContent":["export {\n createServerIdentity,\n deriveDstackPrivateKey,\n isTinyCloudSessionError,\n serverDidForPrivateKey,\n withSessionRefresh,\n type CreateServerIdentityOptions,\n type DeriveDstackPrivateKeyOptions,\n type DstackKeyClient,\n type ServerIdentity,\n} from \"./identity.js\";\n\nexport {\n createServerDelegateClient,\n parseDelegation,\n parseEncryptedEnvelope,\n parseSecretPayload,\n readDelegatedSecret,\n type CreateServerDelegateClientOptions,\n type DelegationInput,\n type ServerDelegateClient,\n} from \"./delegated-secrets.js\";\n\nexport {\n NonceStore,\n ServerAuthError,\n createSiweSession,\n issueSessionToken,\n verifySessionToken,\n verifySiweMessage,\n type CreateSiweSessionOptions,\n type SessionClaims,\n type SessionToken,\n type VerifiedSiwe,\n} from \"./siwe-session.js\";\n","import { keccak256, type Hex } from \"viem\";\nimport { privateKeyToAccount } from \"viem/accounts\";\nimport { TinyCloudNode, type Manifest, type TinyCloudNodeConfig } from \"@tinycloud/node-sdk\";\nimport { authorizationVerdictOf } from \"@tinycloud/sdk-core\";\n\nconst DEFAULT_HOST = \"https://node.tinycloud.xyz\";\n\nexport interface DstackKeyClient {\n getKey(path: string, purpose: string): Promise<{ key: Uint8Array }>;\n}\n\nexport interface DeriveDstackPrivateKeyOptions {\n client: DstackKeyClient;\n path: string;\n purpose: string;\n}\n\nexport async function deriveDstackPrivateKey(\n options: DeriveDstackPrivateKeyOptions,\n): Promise<Hex> {\n const res = await options.client.getKey(options.path, options.purpose);\n if (!(res.key instanceof Uint8Array) || res.key.length === 0) {\n throw new Error(\"dstack getKey returned no key material\");\n }\n return keccak256(res.key);\n}\n\nexport function serverDidForPrivateKey(privateKey: string): string {\n const account = privateKeyToAccount(privateKey as Hex);\n return `did:pkh:eip155:1:${account.address}`;\n}\n\nexport interface CreateServerIdentityOptions {\n privateKey: string;\n host?: string;\n prefix?: string;\n manifest?: Manifest | Manifest[];\n autoCreateSpace?: boolean;\n enablePublicSpace?: boolean;\n includeAccountRegistryPermissions?: boolean;\n nodeConfig?: Omit<\n TinyCloudNodeConfig,\n | \"privateKey\"\n | \"host\"\n | \"prefix\"\n | \"manifest\"\n | \"autoCreateSpace\"\n | \"enablePublicSpace\"\n | \"includeAccountRegistryPermissions\"\n >;\n}\n\nexport interface ServerIdentity {\n node: TinyCloudNode;\n did: string;\n host: string;\n privateKey: string;\n}\n\nexport async function createServerIdentity(\n options: CreateServerIdentityOptions,\n): Promise<ServerIdentity> {\n const host = options.host ?? DEFAULT_HOST;\n const node = new TinyCloudNode({\n ...options.nodeConfig,\n privateKey: options.privateKey,\n host,\n prefix: options.prefix,\n manifest: options.manifest,\n autoCreateSpace: options.autoCreateSpace ?? false,\n enablePublicSpace: options.enablePublicSpace ?? false,\n includeAccountRegistryPermissions: options.includeAccountRegistryPermissions ?? false,\n });\n\n await node.signIn();\n\n return {\n node,\n did: node.did,\n host,\n privateKey: options.privateKey,\n };\n}\n\n// Message heuristics, for untyped errors only.\n//\n// 1. Double-quoted strings are dropped (escape-aware). The SDK writes\n// caller-chosen names such as KV keys with `JSON.stringify`, so a status-like\n// number or quote inside a key can never be read as, or hide, the status.\n// 2. The first number in 400-599 in one of the SDK's diagnostic positions is\n// the status:\n// - `: NNN` followed by whitespace or the end — KV `…key \"k\": 403 - text`,\n// hooks `…webhook: 401 hook ticket expired`, `…with server: 403`,\n// `…failed: 401`\n// - `HTTP NNN`, `returned NNN`, `rejected (NNN)`\n// - a trailing `(NNN)` or `(NNN).`\n// Only a 401 refreshes, so a real 502 or 403 is never overridden by a `401`\n// later in its body.\n// 3. With no status, session wording on the unstripped message decides.\nconst QUOTED_STRING_PATTERN = /\"(?:[^\"\\\\]|\\\\.)*\"/g;\nconst DIAGNOSTIC_STATUS_PATTERN =\n /:\\s(\\d{3})(?=\\s|$)|\\bHTTP\\s(\\d{3})\\b|\\breturned\\s(\\d{3})\\b|\\brejected\\s\\((\\d{3})\\)|\\((\\d{3})\\)\\.?$/gi;\nconst SESSION_ERROR_PATTERN =\n /\\b(session\\s+expired|invalid\\s+session|token\\s+expired|expired\\s+credentials?|unauthorized|unauthenticated|sign.?in\\s*required)\\b/i;\n\n/** The first HTTP error status in a diagnostic position, ignoring quoted strings. */\nfunction diagnosticStatusOf(message: string): number | undefined {\n const unquoted = message.replace(QUOTED_STRING_PATTERN, '\"\"');\n for (const match of unquoted.matchAll(DIAGNOSTIC_STATUS_PATTERN)) {\n const status = Number(match.slice(1).find((group) => group !== undefined));\n if (status >= 400 && status <= 599) return status;\n }\n return undefined;\n}\n\n/**\n * Whether re-signing in can fix `error`. Typed errors (HTTP `status`,\n * `statusCode` or `meta.status`, or `AUTH_UNAUTHORIZED`, including through the\n * `cause` chain) decide from their structure: only a 401 refreshes, so a 403\n * never does whatever its body says. Untyped errors fall back to the message.\n */\nexport function isTinyCloudSessionError(error: unknown): boolean {\n const verdict = authorizationVerdictOf(error);\n if (verdict !== undefined) {\n return verdict === \"unauthenticated\";\n }\n const message = error instanceof Error ? error.message : String(error);\n const status = diagnosticStatusOf(message);\n if (status !== undefined) {\n return status === 401;\n }\n return SESSION_ERROR_PATTERN.test(message);\n}\n\n/**\n * Run `fn`, re-signing in and retrying once when it fails with a session\n * error (see {@link isTinyCloudSessionError}). To keep the typed status when\n * unwrapping a `Result`, throw the `ServiceError` itself or an `Error` whose\n * `cause` is the `ServiceError`.\n */\nexport async function withSessionRefresh<T>(\n node: TinyCloudNode,\n fn: () => Promise<T>,\n): Promise<T> {\n try {\n return await fn();\n } catch (error) {\n if (isTinyCloudSessionError(error)) {\n await node.signIn();\n return fn();\n }\n throw error;\n }\n}\n","import {\n TinyCloudNode,\n deserializeDelegation,\n type InlineEncryptedEnvelope,\n type PortableDelegation,\n type SecretScopeOptions,\n} from \"@tinycloud/node-sdk\";\nimport { resolveSecretPath } from \"@tinycloud/sdk-core\";\nimport type { CreateServerIdentityOptions } from \"./identity.js\";\nimport { createServerIdentity } from \"./identity.js\";\n\ntype KvLike = {\n get<T = unknown>(\n key: string,\n options: { raw: true; prefix: string },\n ): Promise<{ ok: true; data: T | unknown } | { ok: false; error?: { message?: string; meta?: Record<string, unknown> } }>;\n};\n\ntype DelegatedAccessLike = {\n kv: KvLike;\n delegation: Pick<PortableDelegation, \"cid\">;\n restorable?: { delegationCid?: string };\n};\n\ntype DecryptResult =\n | { ok: true; data: Uint8Array }\n | { ok: false; error: { code?: string; message: string } };\n\ntype EncryptionLike = {\n decryptEnvelope(\n envelope: InlineEncryptedEnvelope,\n capabilityProof: { proofs: string[] },\n ): Promise<DecryptResult>;\n};\n\ntype TinyCloudNodeLike = {\n signIn(): Promise<unknown>;\n useDelegation(delegation: PortableDelegation): Promise<DelegatedAccessLike>;\n encryption: EncryptionLike;\n};\n\nexport type DelegationInput = PortableDelegation | string;\n\nexport interface ServerDelegateClient {\n getSecret(name: string, options?: SecretScopeOptions): Promise<string>;\n}\n\nexport interface CreateServerDelegateClientOptions {\n privateKey: string;\n host?: string;\n delegation: DelegationInput;\n prefix?: string;\n nodeConfig?: CreateServerIdentityOptions[\"nodeConfig\"];\n node?: TinyCloudNodeLike;\n nodeFactory?: (options: CreateServerIdentityOptions) => Promise<TinyCloudNodeLike>;\n}\n\nexport function createServerDelegateClient(\n options: CreateServerDelegateClientOptions,\n): ServerDelegateClient {\n const delegation = parseDelegation(options.delegation);\n let nodePromise: Promise<TinyCloudNodeLike> | undefined;\n\n async function getNode(): Promise<TinyCloudNodeLike> {\n if (!nodePromise) {\n if (options.node) {\n nodePromise = Promise.resolve(options.node);\n return options.node;\n }\n const identityOptions: CreateServerIdentityOptions = {\n privateKey: options.privateKey,\n host: options.host,\n prefix: options.prefix,\n enablePublicSpace: false,\n includeAccountRegistryPermissions: false,\n nodeConfig: options.nodeConfig,\n };\n const created = options.nodeFactory\n ? options.nodeFactory(identityOptions)\n : createServerIdentity(identityOptions).then(\n (identity) => identity.node as unknown as TinyCloudNodeLike,\n );\n nodePromise = created;\n return created;\n }\n return nodePromise;\n }\n\n return {\n async getSecret(name: string, secretOptions?: SecretScopeOptions): Promise<string> {\n const node = await getNode();\n return readDelegatedSecret(node, delegation, name, secretOptions);\n },\n };\n}\n\nexport async function readDelegatedSecret(\n node: TinyCloudNodeLike | TinyCloudNode,\n delegation: PortableDelegation,\n name: string,\n options?: SecretScopeOptions,\n): Promise<string> {\n const secretKey = resolveSecretPath(name, options).permissionPaths.vault;\n\n const access = await node.useDelegation(delegation);\n const result = await access.kv.get<unknown>(secretKey, { raw: true, prefix: \"\" });\n if (!result.ok) {\n const message = result.error?.message ?? `failed to read ${secretKey}`;\n throw Object.assign(new Error(`delegated secret ${name} KV get failed: ${message}`), { cause: result.error });\n }\n\n const envelope = parseEncryptedEnvelope(\n (result.data as { data?: unknown } | undefined)?.data,\n name,\n );\n const proofCid = access.restorable?.delegationCid ?? access.delegation.cid;\n if (!proofCid) {\n throw new Error(`delegated secret ${name} has no decrypt proof`);\n }\n\n const decrypted = await node.encryption.decryptEnvelope(envelope, { proofs: [proofCid] });\n if (!decrypted.ok) {\n throw new Error(`delegated secret ${name} decrypt failed: ${decrypted.error.message}`);\n }\n\n return parseSecretPayload(decrypted.data, name);\n}\n\nexport function parseDelegation(delegation: DelegationInput): PortableDelegation {\n return typeof delegation === \"string\" ? deserializeDelegation(delegation) : delegation;\n}\n\nexport function parseEncryptedEnvelope(\n rawEnvelope: unknown,\n name = \"secret\",\n): InlineEncryptedEnvelope {\n const parsed = typeof rawEnvelope === \"string\" ? JSON.parse(rawEnvelope) : rawEnvelope;\n if (\n typeof parsed !== \"object\" ||\n parsed === null ||\n typeof (parsed as Partial<InlineEncryptedEnvelope>).v !== \"number\" ||\n typeof (parsed as Partial<InlineEncryptedEnvelope>).networkId !== \"string\" ||\n typeof (parsed as Partial<InlineEncryptedEnvelope>).ciphertext !== \"string\" ||\n typeof (parsed as Partial<InlineEncryptedEnvelope>).encryptedSymmetricKey !== \"string\"\n ) {\n throw new Error(`delegated secret ${name} did not contain an encrypted envelope`);\n }\n return parsed as InlineEncryptedEnvelope;\n}\n\nexport function parseSecretPayload(plaintext: Uint8Array, name = \"secret\"): string {\n let parsed: { value?: unknown };\n try {\n parsed = JSON.parse(new TextDecoder().decode(plaintext)) as { value?: unknown };\n } catch {\n throw new Error(`delegated secret ${name} did not contain valid JSON`);\n }\n if (typeof parsed.value !== \"string\") {\n throw new Error(`delegated secret ${name} did not contain a string value`);\n }\n return parsed.value;\n}\n","import { createHmac, randomBytes, timingSafeEqual } from \"node:crypto\";\nimport { getAddress, recoverMessageAddress, type Hex } from \"viem\";\n\nconst DEFAULT_NONCE_TTL_MS = 5 * 60 * 1000;\nconst DEFAULT_SESSION_TTL_SECONDS = 24 * 60 * 60;\n\ninterface NonceEntry {\n address: string;\n createdAt: number;\n}\n\nexport class ServerAuthError extends Error {}\n\nexport class NonceStore {\n private readonly nonces = new Map<string, NonceEntry>();\n\n constructor(private readonly ttlMs = DEFAULT_NONCE_TTL_MS) {}\n\n issue(address: string): string {\n this.sweep();\n const normalized = getAddress(address).toLowerCase();\n const nonce = randomBytes(16).toString(\"hex\");\n this.nonces.set(this.key(normalized, nonce), {\n address: normalized,\n createdAt: Date.now(),\n });\n return nonce;\n }\n\n validate(address: string, nonce: string): boolean {\n const normalized = getAddress(address).toLowerCase();\n const key = this.key(normalized, nonce);\n const entry = this.nonces.get(key);\n if (!entry) return false;\n this.nonces.delete(key);\n return Date.now() - entry.createdAt <= this.ttlMs;\n }\n\n private key(address: string, nonce: string): string {\n return `${address}:${nonce}`;\n }\n\n private sweep(): void {\n const now = Date.now();\n for (const [key, entry] of this.nonces) {\n if (now - entry.createdAt > this.ttlMs) this.nonces.delete(key);\n }\n }\n}\n\nexport interface VerifiedSiwe {\n address: string;\n nonce: string;\n}\n\nexport async function verifySiweMessage(\n message: string,\n signature: string,\n): Promise<VerifiedSiwe> {\n const lines = message.split(\"\\n\");\n let claimed: string;\n try {\n claimed = getAddress((lines[1] ?? \"\").trim());\n } catch {\n throw new ServerAuthError(\"SIWE message is missing a valid address on line 2\");\n }\n\n const nonceMatch = message.match(/^Nonce: (.+)$/m);\n if (!nonceMatch || nonceMatch[1] === undefined) {\n throw new ServerAuthError(\"SIWE message is missing a Nonce line\");\n }\n const nonce = nonceMatch[1].trim();\n\n let recovered: string;\n try {\n recovered = await recoverMessageAddress({ message, signature: signature as Hex });\n } catch (error) {\n throw new ServerAuthError(\n `SIWE signature recovery failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n if (getAddress(recovered) !== claimed) {\n throw new ServerAuthError(\n `SIWE signature does not match message address (recovered ${recovered}, expected ${claimed})`,\n );\n }\n\n return { address: claimed, nonce };\n}\n\nexport interface SessionToken {\n token: string;\n expiresIn: number;\n}\n\nexport interface SessionClaims {\n sub: string;\n address: string;\n iat: number;\n exp: number;\n}\n\nexport function issueSessionToken(\n address: string,\n secret: string,\n ttlSeconds = DEFAULT_SESSION_TTL_SECONDS,\n): SessionToken {\n const normalized = getAddress(address);\n const now = Math.floor(Date.now() / 1000);\n const claims: SessionClaims = {\n sub: normalized,\n address: normalized,\n iat: now,\n exp: now + ttlSeconds,\n };\n return {\n token: signJwt({ alg: \"HS256\", typ: \"JWT\" }, claims, secret),\n expiresIn: ttlSeconds,\n };\n}\n\nexport function verifySessionToken(token: string, secret: string): { address: string } {\n const claims = verifyJwt(token, secret);\n if (typeof claims.sub !== \"string\" || claims.sub === \"\") {\n throw new ServerAuthError(\"session token missing 'sub' claim\");\n }\n return { address: claims.sub };\n}\n\nexport interface CreateSiweSessionOptions {\n jwtSecret: string;\n nonceStore?: NonceStore;\n sessionTtlSeconds?: number;\n}\n\nexport function createSiweSession(options: CreateSiweSessionOptions) {\n const nonceStore = options.nonceStore ?? new NonceStore();\n const sessionTtlSeconds = options.sessionTtlSeconds ?? DEFAULT_SESSION_TTL_SECONDS;\n\n return {\n issueNonce(address: string): string {\n return nonceStore.issue(address);\n },\n async verify(message: string, signature: string): Promise<SessionToken> {\n const verified = await verifySiweMessage(message, signature);\n if (!nonceStore.validate(verified.address, verified.nonce)) {\n throw new ServerAuthError(\"nonce is invalid, expired, or already used\");\n }\n return issueSessionToken(verified.address, options.jwtSecret, sessionTtlSeconds);\n },\n verifyToken(token: string): { address: string } {\n return verifySessionToken(token, options.jwtSecret);\n },\n };\n}\n\nfunction signJwt(header: object, payload: object, secret: string): string {\n const encodedHeader = base64UrlEncode(JSON.stringify(header));\n const encodedPayload = base64UrlEncode(JSON.stringify(payload));\n const signingInput = `${encodedHeader}.${encodedPayload}`;\n const signature = createHmac(\"sha256\", secret).update(signingInput).digest();\n return `${signingInput}.${base64UrlEncode(signature)}`;\n}\n\nfunction verifyJwt(token: string, secret: string): SessionClaims {\n const parts = token.split(\".\");\n if (parts.length !== 3 || !parts[0] || !parts[1] || !parts[2]) {\n throw new ServerAuthError(\"session token is not a valid JWT\");\n }\n\n const signingInput = `${parts[0]}.${parts[1]}`;\n const expected = createHmac(\"sha256\", secret).update(signingInput).digest();\n const actual = base64UrlDecode(parts[2]);\n if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) {\n throw new ServerAuthError(\"session token signature verification failed\");\n }\n\n const claims = JSON.parse(base64UrlDecode(parts[1]).toString(\"utf8\")) as SessionClaims;\n if (typeof claims.exp !== \"number\" || claims.exp <= Math.floor(Date.now() / 1000)) {\n throw new ServerAuthError(\"session token expired\");\n }\n return claims;\n}\n\nfunction base64UrlEncode(value: string | Buffer): string {\n const buffer = typeof value === \"string\" ? Buffer.from(value, \"utf8\") : value;\n return buffer.toString(\"base64url\");\n}\n\nfunction base64UrlDecode(value: string): Buffer {\n return Buffer.from(value, \"base64url\");\n}\n"],"mappings":"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","names":["import_node_sdk","import_sdk_core","import_viem"]}
|
package/dist/index.d.cts
CHANGED
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -113,7 +113,7 @@ async function readDelegatedSecret(node, delegation, name, options) {
|
|
|
113
113
|
const result = await access.kv.get(secretKey, { raw: true, prefix: "" });
|
|
114
114
|
if (!result.ok) {
|
|
115
115
|
const message = result.error?.message ?? `failed to read ${secretKey}`;
|
|
116
|
-
throw new Error(`delegated secret ${name} KV get failed: ${message}`);
|
|
116
|
+
throw Object.assign(new Error(`delegated secret ${name} KV get failed: ${message}`), { cause: result.error });
|
|
117
117
|
}
|
|
118
118
|
const envelope = parseEncryptedEnvelope(
|
|
119
119
|
result.data?.data,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/identity.ts","../src/delegated-secrets.ts","../src/siwe-session.ts"],"sourcesContent":["import { keccak256, type Hex } from \"viem\";\nimport { privateKeyToAccount } from \"viem/accounts\";\nimport { TinyCloudNode, type Manifest, type TinyCloudNodeConfig } from \"@tinycloud/node-sdk\";\nimport { authorizationVerdictOf } from \"@tinycloud/sdk-core\";\n\nconst DEFAULT_HOST = \"https://node.tinycloud.xyz\";\n\nexport interface DstackKeyClient {\n getKey(path: string, purpose: string): Promise<{ key: Uint8Array }>;\n}\n\nexport interface DeriveDstackPrivateKeyOptions {\n client: DstackKeyClient;\n path: string;\n purpose: string;\n}\n\nexport async function deriveDstackPrivateKey(\n options: DeriveDstackPrivateKeyOptions,\n): Promise<Hex> {\n const res = await options.client.getKey(options.path, options.purpose);\n if (!(res.key instanceof Uint8Array) || res.key.length === 0) {\n throw new Error(\"dstack getKey returned no key material\");\n }\n return keccak256(res.key);\n}\n\nexport function serverDidForPrivateKey(privateKey: string): string {\n const account = privateKeyToAccount(privateKey as Hex);\n return `did:pkh:eip155:1:${account.address}`;\n}\n\nexport interface CreateServerIdentityOptions {\n privateKey: string;\n host?: string;\n prefix?: string;\n manifest?: Manifest | Manifest[];\n autoCreateSpace?: boolean;\n enablePublicSpace?: boolean;\n includeAccountRegistryPermissions?: boolean;\n nodeConfig?: Omit<\n TinyCloudNodeConfig,\n | \"privateKey\"\n | \"host\"\n | \"prefix\"\n | \"manifest\"\n | \"autoCreateSpace\"\n | \"enablePublicSpace\"\n | \"includeAccountRegistryPermissions\"\n >;\n}\n\nexport interface ServerIdentity {\n node: TinyCloudNode;\n did: string;\n host: string;\n privateKey: string;\n}\n\nexport async function createServerIdentity(\n options: CreateServerIdentityOptions,\n): Promise<ServerIdentity> {\n const host = options.host ?? DEFAULT_HOST;\n const node = new TinyCloudNode({\n ...options.nodeConfig,\n privateKey: options.privateKey,\n host,\n prefix: options.prefix,\n manifest: options.manifest,\n autoCreateSpace: options.autoCreateSpace ?? false,\n enablePublicSpace: options.enablePublicSpace ?? false,\n includeAccountRegistryPermissions: options.includeAccountRegistryPermissions ?? false,\n });\n\n await node.signIn();\n\n return {\n node,\n did: node.did,\n host,\n privateKey: options.privateKey,\n };\n}\n\n// Message heuristics, for untyped errors only.\n//\n// 1. Double-quoted strings are dropped (escape-aware). The SDK writes\n// caller-chosen names such as KV keys with `JSON.stringify`, so a status-like\n// number or quote inside a key can never be read as, or hide, the status.\n// 2. The first number in 400-599 in one of the SDK's diagnostic positions is\n// the status:\n// - `: NNN` followed by whitespace or the end — KV `…key \"k\": 403 - text`,\n// hooks `…webhook: 401 hook ticket expired`, `…with server: 403`,\n// `…failed: 401`\n// - `HTTP NNN`, `returned NNN`, `rejected (NNN)`\n// - a trailing `(NNN)` or `(NNN).`\n// Only a 401 refreshes, so a real 502 or 403 is never overridden by a `401`\n// later in its body.\n// 3. With no status, session wording on the unstripped message decides.\nconst QUOTED_STRING_PATTERN = /\"(?:[^\"\\\\]|\\\\.)*\"/g;\nconst DIAGNOSTIC_STATUS_PATTERN =\n /:\\s(\\d{3})(?=\\s|$)|\\bHTTP\\s(\\d{3})\\b|\\breturned\\s(\\d{3})\\b|\\brejected\\s\\((\\d{3})\\)|\\((\\d{3})\\)\\.?$/gi;\nconst SESSION_ERROR_PATTERN =\n /\\b(session\\s+expired|invalid\\s+session|token\\s+expired|expired\\s+credentials?|unauthorized|unauthenticated|sign.?in\\s*required)\\b/i;\n\n/** The first HTTP error status in a diagnostic position, ignoring quoted strings. */\nfunction diagnosticStatusOf(message: string): number | undefined {\n const unquoted = message.replace(QUOTED_STRING_PATTERN, '\"\"');\n for (const match of unquoted.matchAll(DIAGNOSTIC_STATUS_PATTERN)) {\n const status = Number(match.slice(1).find((group) => group !== undefined));\n if (status >= 400 && status <= 599) return status;\n }\n return undefined;\n}\n\n/**\n * Whether re-signing in can fix `error`. Typed errors (HTTP `status`,\n * `statusCode` or `meta.status`, or `AUTH_UNAUTHORIZED`, including through the\n * `cause` chain) decide from their structure: only a 401 refreshes, so a 403\n * never does whatever its body says. Untyped errors fall back to the message.\n */\nexport function isTinyCloudSessionError(error: unknown): boolean {\n const verdict = authorizationVerdictOf(error);\n if (verdict !== undefined) {\n return verdict === \"unauthenticated\";\n }\n const message = error instanceof Error ? error.message : String(error);\n const status = diagnosticStatusOf(message);\n if (status !== undefined) {\n return status === 401;\n }\n return SESSION_ERROR_PATTERN.test(message);\n}\n\n/**\n * Run `fn`, re-signing in and retrying once when it fails with a session\n * error (see {@link isTinyCloudSessionError}). To keep the typed status when\n * unwrapping a `Result`, throw the `ServiceError` itself or an `Error` whose\n * `cause` is the `ServiceError`.\n */\nexport async function withSessionRefresh<T>(\n node: TinyCloudNode,\n fn: () => Promise<T>,\n): Promise<T> {\n try {\n return await fn();\n } catch (error) {\n if (isTinyCloudSessionError(error)) {\n await node.signIn();\n return fn();\n }\n throw error;\n }\n}\n","import {\n TinyCloudNode,\n deserializeDelegation,\n type InlineEncryptedEnvelope,\n type PortableDelegation,\n type SecretScopeOptions,\n} from \"@tinycloud/node-sdk\";\nimport { resolveSecretPath } from \"@tinycloud/sdk-core\";\nimport type { CreateServerIdentityOptions } from \"./identity.js\";\nimport { createServerIdentity } from \"./identity.js\";\n\ntype KvLike = {\n get<T = unknown>(\n key: string,\n options: { raw: true; prefix: string },\n ): Promise<{ ok: true; data: T | unknown } | { ok: false; error?: { message?: string } }>;\n};\n\ntype DelegatedAccessLike = {\n kv: KvLike;\n delegation: Pick<PortableDelegation, \"cid\">;\n restorable?: { delegationCid?: string };\n};\n\ntype DecryptResult =\n | { ok: true; data: Uint8Array }\n | { ok: false; error: { code?: string; message: string } };\n\ntype EncryptionLike = {\n decryptEnvelope(\n envelope: InlineEncryptedEnvelope,\n capabilityProof: { proofs: string[] },\n ): Promise<DecryptResult>;\n};\n\ntype TinyCloudNodeLike = {\n signIn(): Promise<unknown>;\n useDelegation(delegation: PortableDelegation): Promise<DelegatedAccessLike>;\n encryption: EncryptionLike;\n};\n\nexport type DelegationInput = PortableDelegation | string;\n\nexport interface ServerDelegateClient {\n getSecret(name: string, options?: SecretScopeOptions): Promise<string>;\n}\n\nexport interface CreateServerDelegateClientOptions {\n privateKey: string;\n host?: string;\n delegation: DelegationInput;\n prefix?: string;\n nodeConfig?: CreateServerIdentityOptions[\"nodeConfig\"];\n node?: TinyCloudNodeLike;\n nodeFactory?: (options: CreateServerIdentityOptions) => Promise<TinyCloudNodeLike>;\n}\n\nexport function createServerDelegateClient(\n options: CreateServerDelegateClientOptions,\n): ServerDelegateClient {\n const delegation = parseDelegation(options.delegation);\n let nodePromise: Promise<TinyCloudNodeLike> | undefined;\n\n async function getNode(): Promise<TinyCloudNodeLike> {\n if (!nodePromise) {\n if (options.node) {\n nodePromise = Promise.resolve(options.node);\n return options.node;\n }\n const identityOptions: CreateServerIdentityOptions = {\n privateKey: options.privateKey,\n host: options.host,\n prefix: options.prefix,\n enablePublicSpace: false,\n includeAccountRegistryPermissions: false,\n nodeConfig: options.nodeConfig,\n };\n const created = options.nodeFactory\n ? options.nodeFactory(identityOptions)\n : createServerIdentity(identityOptions).then(\n (identity) => identity.node as unknown as TinyCloudNodeLike,\n );\n nodePromise = created;\n return created;\n }\n return nodePromise;\n }\n\n return {\n async getSecret(name: string, secretOptions?: SecretScopeOptions): Promise<string> {\n const node = await getNode();\n return readDelegatedSecret(node, delegation, name, secretOptions);\n },\n };\n}\n\nexport async function readDelegatedSecret(\n node: TinyCloudNodeLike | TinyCloudNode,\n delegation: PortableDelegation,\n name: string,\n options?: SecretScopeOptions,\n): Promise<string> {\n const secretKey = resolveSecretPath(name, options).permissionPaths.vault;\n\n const access = await node.useDelegation(delegation);\n const result = await access.kv.get<unknown>(secretKey, { raw: true, prefix: \"\" });\n if (!result.ok) {\n const message = result.error?.message ?? `failed to read ${secretKey}`;\n throw new Error(`delegated secret ${name} KV get failed: ${message}`);\n }\n\n const envelope = parseEncryptedEnvelope(\n (result.data as { data?: unknown } | undefined)?.data,\n name,\n );\n const proofCid = access.restorable?.delegationCid ?? access.delegation.cid;\n if (!proofCid) {\n throw new Error(`delegated secret ${name} has no decrypt proof`);\n }\n\n const decrypted = await node.encryption.decryptEnvelope(envelope, { proofs: [proofCid] });\n if (!decrypted.ok) {\n throw new Error(`delegated secret ${name} decrypt failed: ${decrypted.error.message}`);\n }\n\n return parseSecretPayload(decrypted.data, name);\n}\n\nexport function parseDelegation(delegation: DelegationInput): PortableDelegation {\n return typeof delegation === \"string\" ? deserializeDelegation(delegation) : delegation;\n}\n\nexport function parseEncryptedEnvelope(\n rawEnvelope: unknown,\n name = \"secret\",\n): InlineEncryptedEnvelope {\n const parsed = typeof rawEnvelope === \"string\" ? JSON.parse(rawEnvelope) : rawEnvelope;\n if (\n typeof parsed !== \"object\" ||\n parsed === null ||\n typeof (parsed as Partial<InlineEncryptedEnvelope>).v !== \"number\" ||\n typeof (parsed as Partial<InlineEncryptedEnvelope>).networkId !== \"string\" ||\n typeof (parsed as Partial<InlineEncryptedEnvelope>).ciphertext !== \"string\" ||\n typeof (parsed as Partial<InlineEncryptedEnvelope>).encryptedSymmetricKey !== \"string\"\n ) {\n throw new Error(`delegated secret ${name} did not contain an encrypted envelope`);\n }\n return parsed as InlineEncryptedEnvelope;\n}\n\nexport function parseSecretPayload(plaintext: Uint8Array, name = \"secret\"): string {\n let parsed: { value?: unknown };\n try {\n parsed = JSON.parse(new TextDecoder().decode(plaintext)) as { value?: unknown };\n } catch {\n throw new Error(`delegated secret ${name} did not contain valid JSON`);\n }\n if (typeof parsed.value !== \"string\") {\n throw new Error(`delegated secret ${name} did not contain a string value`);\n }\n return parsed.value;\n}\n","import { createHmac, randomBytes, timingSafeEqual } from \"node:crypto\";\nimport { getAddress, recoverMessageAddress, type Hex } from \"viem\";\n\nconst DEFAULT_NONCE_TTL_MS = 5 * 60 * 1000;\nconst DEFAULT_SESSION_TTL_SECONDS = 24 * 60 * 60;\n\ninterface NonceEntry {\n address: string;\n createdAt: number;\n}\n\nexport class ServerAuthError extends Error {}\n\nexport class NonceStore {\n private readonly nonces = new Map<string, NonceEntry>();\n\n constructor(private readonly ttlMs = DEFAULT_NONCE_TTL_MS) {}\n\n issue(address: string): string {\n this.sweep();\n const normalized = getAddress(address).toLowerCase();\n const nonce = randomBytes(16).toString(\"hex\");\n this.nonces.set(this.key(normalized, nonce), {\n address: normalized,\n createdAt: Date.now(),\n });\n return nonce;\n }\n\n validate(address: string, nonce: string): boolean {\n const normalized = getAddress(address).toLowerCase();\n const key = this.key(normalized, nonce);\n const entry = this.nonces.get(key);\n if (!entry) return false;\n this.nonces.delete(key);\n return Date.now() - entry.createdAt <= this.ttlMs;\n }\n\n private key(address: string, nonce: string): string {\n return `${address}:${nonce}`;\n }\n\n private sweep(): void {\n const now = Date.now();\n for (const [key, entry] of this.nonces) {\n if (now - entry.createdAt > this.ttlMs) this.nonces.delete(key);\n }\n }\n}\n\nexport interface VerifiedSiwe {\n address: string;\n nonce: string;\n}\n\nexport async function verifySiweMessage(\n message: string,\n signature: string,\n): Promise<VerifiedSiwe> {\n const lines = message.split(\"\\n\");\n let claimed: string;\n try {\n claimed = getAddress((lines[1] ?? \"\").trim());\n } catch {\n throw new ServerAuthError(\"SIWE message is missing a valid address on line 2\");\n }\n\n const nonceMatch = message.match(/^Nonce: (.+)$/m);\n if (!nonceMatch || nonceMatch[1] === undefined) {\n throw new ServerAuthError(\"SIWE message is missing a Nonce line\");\n }\n const nonce = nonceMatch[1].trim();\n\n let recovered: string;\n try {\n recovered = await recoverMessageAddress({ message, signature: signature as Hex });\n } catch (error) {\n throw new ServerAuthError(\n `SIWE signature recovery failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n if (getAddress(recovered) !== claimed) {\n throw new ServerAuthError(\n `SIWE signature does not match message address (recovered ${recovered}, expected ${claimed})`,\n );\n }\n\n return { address: claimed, nonce };\n}\n\nexport interface SessionToken {\n token: string;\n expiresIn: number;\n}\n\nexport interface SessionClaims {\n sub: string;\n address: string;\n iat: number;\n exp: number;\n}\n\nexport function issueSessionToken(\n address: string,\n secret: string,\n ttlSeconds = DEFAULT_SESSION_TTL_SECONDS,\n): SessionToken {\n const normalized = getAddress(address);\n const now = Math.floor(Date.now() / 1000);\n const claims: SessionClaims = {\n sub: normalized,\n address: normalized,\n iat: now,\n exp: now + ttlSeconds,\n };\n return {\n token: signJwt({ alg: \"HS256\", typ: \"JWT\" }, claims, secret),\n expiresIn: ttlSeconds,\n };\n}\n\nexport function verifySessionToken(token: string, secret: string): { address: string } {\n const claims = verifyJwt(token, secret);\n if (typeof claims.sub !== \"string\" || claims.sub === \"\") {\n throw new ServerAuthError(\"session token missing 'sub' claim\");\n }\n return { address: claims.sub };\n}\n\nexport interface CreateSiweSessionOptions {\n jwtSecret: string;\n nonceStore?: NonceStore;\n sessionTtlSeconds?: number;\n}\n\nexport function createSiweSession(options: CreateSiweSessionOptions) {\n const nonceStore = options.nonceStore ?? new NonceStore();\n const sessionTtlSeconds = options.sessionTtlSeconds ?? DEFAULT_SESSION_TTL_SECONDS;\n\n return {\n issueNonce(address: string): string {\n return nonceStore.issue(address);\n },\n async verify(message: string, signature: string): Promise<SessionToken> {\n const verified = await verifySiweMessage(message, signature);\n if (!nonceStore.validate(verified.address, verified.nonce)) {\n throw new ServerAuthError(\"nonce is invalid, expired, or already used\");\n }\n return issueSessionToken(verified.address, options.jwtSecret, sessionTtlSeconds);\n },\n verifyToken(token: string): { address: string } {\n return verifySessionToken(token, options.jwtSecret);\n },\n };\n}\n\nfunction signJwt(header: object, payload: object, secret: string): string {\n const encodedHeader = base64UrlEncode(JSON.stringify(header));\n const encodedPayload = base64UrlEncode(JSON.stringify(payload));\n const signingInput = `${encodedHeader}.${encodedPayload}`;\n const signature = createHmac(\"sha256\", secret).update(signingInput).digest();\n return `${signingInput}.${base64UrlEncode(signature)}`;\n}\n\nfunction verifyJwt(token: string, secret: string): SessionClaims {\n const parts = token.split(\".\");\n if (parts.length !== 3 || !parts[0] || !parts[1] || !parts[2]) {\n throw new ServerAuthError(\"session token is not a valid JWT\");\n }\n\n const signingInput = `${parts[0]}.${parts[1]}`;\n const expected = createHmac(\"sha256\", secret).update(signingInput).digest();\n const actual = base64UrlDecode(parts[2]);\n if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) {\n throw new ServerAuthError(\"session token signature verification failed\");\n }\n\n const claims = JSON.parse(base64UrlDecode(parts[1]).toString(\"utf8\")) as SessionClaims;\n if (typeof claims.exp !== \"number\" || claims.exp <= Math.floor(Date.now() / 1000)) {\n throw new ServerAuthError(\"session token expired\");\n }\n return claims;\n}\n\nfunction base64UrlEncode(value: string | Buffer): string {\n const buffer = typeof value === \"string\" ? Buffer.from(value, \"utf8\") : value;\n return buffer.toString(\"base64url\");\n}\n\nfunction base64UrlDecode(value: string): Buffer {\n return Buffer.from(value, \"base64url\");\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/identity.ts","../src/delegated-secrets.ts","../src/siwe-session.ts"],"sourcesContent":["import { keccak256, type Hex } from \"viem\";\nimport { privateKeyToAccount } from \"viem/accounts\";\nimport { TinyCloudNode, type Manifest, type TinyCloudNodeConfig } from \"@tinycloud/node-sdk\";\nimport { authorizationVerdictOf } from \"@tinycloud/sdk-core\";\n\nconst DEFAULT_HOST = \"https://node.tinycloud.xyz\";\n\nexport interface DstackKeyClient {\n getKey(path: string, purpose: string): Promise<{ key: Uint8Array }>;\n}\n\nexport interface DeriveDstackPrivateKeyOptions {\n client: DstackKeyClient;\n path: string;\n purpose: string;\n}\n\nexport async function deriveDstackPrivateKey(\n options: DeriveDstackPrivateKeyOptions,\n): Promise<Hex> {\n const res = await options.client.getKey(options.path, options.purpose);\n if (!(res.key instanceof Uint8Array) || res.key.length === 0) {\n throw new Error(\"dstack getKey returned no key material\");\n }\n return keccak256(res.key);\n}\n\nexport function serverDidForPrivateKey(privateKey: string): string {\n const account = privateKeyToAccount(privateKey as Hex);\n return `did:pkh:eip155:1:${account.address}`;\n}\n\nexport interface CreateServerIdentityOptions {\n privateKey: string;\n host?: string;\n prefix?: string;\n manifest?: Manifest | Manifest[];\n autoCreateSpace?: boolean;\n enablePublicSpace?: boolean;\n includeAccountRegistryPermissions?: boolean;\n nodeConfig?: Omit<\n TinyCloudNodeConfig,\n | \"privateKey\"\n | \"host\"\n | \"prefix\"\n | \"manifest\"\n | \"autoCreateSpace\"\n | \"enablePublicSpace\"\n | \"includeAccountRegistryPermissions\"\n >;\n}\n\nexport interface ServerIdentity {\n node: TinyCloudNode;\n did: string;\n host: string;\n privateKey: string;\n}\n\nexport async function createServerIdentity(\n options: CreateServerIdentityOptions,\n): Promise<ServerIdentity> {\n const host = options.host ?? DEFAULT_HOST;\n const node = new TinyCloudNode({\n ...options.nodeConfig,\n privateKey: options.privateKey,\n host,\n prefix: options.prefix,\n manifest: options.manifest,\n autoCreateSpace: options.autoCreateSpace ?? false,\n enablePublicSpace: options.enablePublicSpace ?? false,\n includeAccountRegistryPermissions: options.includeAccountRegistryPermissions ?? false,\n });\n\n await node.signIn();\n\n return {\n node,\n did: node.did,\n host,\n privateKey: options.privateKey,\n };\n}\n\n// Message heuristics, for untyped errors only.\n//\n// 1. Double-quoted strings are dropped (escape-aware). The SDK writes\n// caller-chosen names such as KV keys with `JSON.stringify`, so a status-like\n// number or quote inside a key can never be read as, or hide, the status.\n// 2. The first number in 400-599 in one of the SDK's diagnostic positions is\n// the status:\n// - `: NNN` followed by whitespace or the end — KV `…key \"k\": 403 - text`,\n// hooks `…webhook: 401 hook ticket expired`, `…with server: 403`,\n// `…failed: 401`\n// - `HTTP NNN`, `returned NNN`, `rejected (NNN)`\n// - a trailing `(NNN)` or `(NNN).`\n// Only a 401 refreshes, so a real 502 or 403 is never overridden by a `401`\n// later in its body.\n// 3. With no status, session wording on the unstripped message decides.\nconst QUOTED_STRING_PATTERN = /\"(?:[^\"\\\\]|\\\\.)*\"/g;\nconst DIAGNOSTIC_STATUS_PATTERN =\n /:\\s(\\d{3})(?=\\s|$)|\\bHTTP\\s(\\d{3})\\b|\\breturned\\s(\\d{3})\\b|\\brejected\\s\\((\\d{3})\\)|\\((\\d{3})\\)\\.?$/gi;\nconst SESSION_ERROR_PATTERN =\n /\\b(session\\s+expired|invalid\\s+session|token\\s+expired|expired\\s+credentials?|unauthorized|unauthenticated|sign.?in\\s*required)\\b/i;\n\n/** The first HTTP error status in a diagnostic position, ignoring quoted strings. */\nfunction diagnosticStatusOf(message: string): number | undefined {\n const unquoted = message.replace(QUOTED_STRING_PATTERN, '\"\"');\n for (const match of unquoted.matchAll(DIAGNOSTIC_STATUS_PATTERN)) {\n const status = Number(match.slice(1).find((group) => group !== undefined));\n if (status >= 400 && status <= 599) return status;\n }\n return undefined;\n}\n\n/**\n * Whether re-signing in can fix `error`. Typed errors (HTTP `status`,\n * `statusCode` or `meta.status`, or `AUTH_UNAUTHORIZED`, including through the\n * `cause` chain) decide from their structure: only a 401 refreshes, so a 403\n * never does whatever its body says. Untyped errors fall back to the message.\n */\nexport function isTinyCloudSessionError(error: unknown): boolean {\n const verdict = authorizationVerdictOf(error);\n if (verdict !== undefined) {\n return verdict === \"unauthenticated\";\n }\n const message = error instanceof Error ? error.message : String(error);\n const status = diagnosticStatusOf(message);\n if (status !== undefined) {\n return status === 401;\n }\n return SESSION_ERROR_PATTERN.test(message);\n}\n\n/**\n * Run `fn`, re-signing in and retrying once when it fails with a session\n * error (see {@link isTinyCloudSessionError}). To keep the typed status when\n * unwrapping a `Result`, throw the `ServiceError` itself or an `Error` whose\n * `cause` is the `ServiceError`.\n */\nexport async function withSessionRefresh<T>(\n node: TinyCloudNode,\n fn: () => Promise<T>,\n): Promise<T> {\n try {\n return await fn();\n } catch (error) {\n if (isTinyCloudSessionError(error)) {\n await node.signIn();\n return fn();\n }\n throw error;\n }\n}\n","import {\n TinyCloudNode,\n deserializeDelegation,\n type InlineEncryptedEnvelope,\n type PortableDelegation,\n type SecretScopeOptions,\n} from \"@tinycloud/node-sdk\";\nimport { resolveSecretPath } from \"@tinycloud/sdk-core\";\nimport type { CreateServerIdentityOptions } from \"./identity.js\";\nimport { createServerIdentity } from \"./identity.js\";\n\ntype KvLike = {\n get<T = unknown>(\n key: string,\n options: { raw: true; prefix: string },\n ): Promise<{ ok: true; data: T | unknown } | { ok: false; error?: { message?: string; meta?: Record<string, unknown> } }>;\n};\n\ntype DelegatedAccessLike = {\n kv: KvLike;\n delegation: Pick<PortableDelegation, \"cid\">;\n restorable?: { delegationCid?: string };\n};\n\ntype DecryptResult =\n | { ok: true; data: Uint8Array }\n | { ok: false; error: { code?: string; message: string } };\n\ntype EncryptionLike = {\n decryptEnvelope(\n envelope: InlineEncryptedEnvelope,\n capabilityProof: { proofs: string[] },\n ): Promise<DecryptResult>;\n};\n\ntype TinyCloudNodeLike = {\n signIn(): Promise<unknown>;\n useDelegation(delegation: PortableDelegation): Promise<DelegatedAccessLike>;\n encryption: EncryptionLike;\n};\n\nexport type DelegationInput = PortableDelegation | string;\n\nexport interface ServerDelegateClient {\n getSecret(name: string, options?: SecretScopeOptions): Promise<string>;\n}\n\nexport interface CreateServerDelegateClientOptions {\n privateKey: string;\n host?: string;\n delegation: DelegationInput;\n prefix?: string;\n nodeConfig?: CreateServerIdentityOptions[\"nodeConfig\"];\n node?: TinyCloudNodeLike;\n nodeFactory?: (options: CreateServerIdentityOptions) => Promise<TinyCloudNodeLike>;\n}\n\nexport function createServerDelegateClient(\n options: CreateServerDelegateClientOptions,\n): ServerDelegateClient {\n const delegation = parseDelegation(options.delegation);\n let nodePromise: Promise<TinyCloudNodeLike> | undefined;\n\n async function getNode(): Promise<TinyCloudNodeLike> {\n if (!nodePromise) {\n if (options.node) {\n nodePromise = Promise.resolve(options.node);\n return options.node;\n }\n const identityOptions: CreateServerIdentityOptions = {\n privateKey: options.privateKey,\n host: options.host,\n prefix: options.prefix,\n enablePublicSpace: false,\n includeAccountRegistryPermissions: false,\n nodeConfig: options.nodeConfig,\n };\n const created = options.nodeFactory\n ? options.nodeFactory(identityOptions)\n : createServerIdentity(identityOptions).then(\n (identity) => identity.node as unknown as TinyCloudNodeLike,\n );\n nodePromise = created;\n return created;\n }\n return nodePromise;\n }\n\n return {\n async getSecret(name: string, secretOptions?: SecretScopeOptions): Promise<string> {\n const node = await getNode();\n return readDelegatedSecret(node, delegation, name, secretOptions);\n },\n };\n}\n\nexport async function readDelegatedSecret(\n node: TinyCloudNodeLike | TinyCloudNode,\n delegation: PortableDelegation,\n name: string,\n options?: SecretScopeOptions,\n): Promise<string> {\n const secretKey = resolveSecretPath(name, options).permissionPaths.vault;\n\n const access = await node.useDelegation(delegation);\n const result = await access.kv.get<unknown>(secretKey, { raw: true, prefix: \"\" });\n if (!result.ok) {\n const message = result.error?.message ?? `failed to read ${secretKey}`;\n throw Object.assign(new Error(`delegated secret ${name} KV get failed: ${message}`), { cause: result.error });\n }\n\n const envelope = parseEncryptedEnvelope(\n (result.data as { data?: unknown } | undefined)?.data,\n name,\n );\n const proofCid = access.restorable?.delegationCid ?? access.delegation.cid;\n if (!proofCid) {\n throw new Error(`delegated secret ${name} has no decrypt proof`);\n }\n\n const decrypted = await node.encryption.decryptEnvelope(envelope, { proofs: [proofCid] });\n if (!decrypted.ok) {\n throw new Error(`delegated secret ${name} decrypt failed: ${decrypted.error.message}`);\n }\n\n return parseSecretPayload(decrypted.data, name);\n}\n\nexport function parseDelegation(delegation: DelegationInput): PortableDelegation {\n return typeof delegation === \"string\" ? deserializeDelegation(delegation) : delegation;\n}\n\nexport function parseEncryptedEnvelope(\n rawEnvelope: unknown,\n name = \"secret\",\n): InlineEncryptedEnvelope {\n const parsed = typeof rawEnvelope === \"string\" ? JSON.parse(rawEnvelope) : rawEnvelope;\n if (\n typeof parsed !== \"object\" ||\n parsed === null ||\n typeof (parsed as Partial<InlineEncryptedEnvelope>).v !== \"number\" ||\n typeof (parsed as Partial<InlineEncryptedEnvelope>).networkId !== \"string\" ||\n typeof (parsed as Partial<InlineEncryptedEnvelope>).ciphertext !== \"string\" ||\n typeof (parsed as Partial<InlineEncryptedEnvelope>).encryptedSymmetricKey !== \"string\"\n ) {\n throw new Error(`delegated secret ${name} did not contain an encrypted envelope`);\n }\n return parsed as InlineEncryptedEnvelope;\n}\n\nexport function parseSecretPayload(plaintext: Uint8Array, name = \"secret\"): string {\n let parsed: { value?: unknown };\n try {\n parsed = JSON.parse(new TextDecoder().decode(plaintext)) as { value?: unknown };\n } catch {\n throw new Error(`delegated secret ${name} did not contain valid JSON`);\n }\n if (typeof parsed.value !== \"string\") {\n throw new Error(`delegated secret ${name} did not contain a string value`);\n }\n return parsed.value;\n}\n","import { createHmac, randomBytes, timingSafeEqual } from \"node:crypto\";\nimport { getAddress, recoverMessageAddress, type Hex } from \"viem\";\n\nconst DEFAULT_NONCE_TTL_MS = 5 * 60 * 1000;\nconst DEFAULT_SESSION_TTL_SECONDS = 24 * 60 * 60;\n\ninterface NonceEntry {\n address: string;\n createdAt: number;\n}\n\nexport class ServerAuthError extends Error {}\n\nexport class NonceStore {\n private readonly nonces = new Map<string, NonceEntry>();\n\n constructor(private readonly ttlMs = DEFAULT_NONCE_TTL_MS) {}\n\n issue(address: string): string {\n this.sweep();\n const normalized = getAddress(address).toLowerCase();\n const nonce = randomBytes(16).toString(\"hex\");\n this.nonces.set(this.key(normalized, nonce), {\n address: normalized,\n createdAt: Date.now(),\n });\n return nonce;\n }\n\n validate(address: string, nonce: string): boolean {\n const normalized = getAddress(address).toLowerCase();\n const key = this.key(normalized, nonce);\n const entry = this.nonces.get(key);\n if (!entry) return false;\n this.nonces.delete(key);\n return Date.now() - entry.createdAt <= this.ttlMs;\n }\n\n private key(address: string, nonce: string): string {\n return `${address}:${nonce}`;\n }\n\n private sweep(): void {\n const now = Date.now();\n for (const [key, entry] of this.nonces) {\n if (now - entry.createdAt > this.ttlMs) this.nonces.delete(key);\n }\n }\n}\n\nexport interface VerifiedSiwe {\n address: string;\n nonce: string;\n}\n\nexport async function verifySiweMessage(\n message: string,\n signature: string,\n): Promise<VerifiedSiwe> {\n const lines = message.split(\"\\n\");\n let claimed: string;\n try {\n claimed = getAddress((lines[1] ?? \"\").trim());\n } catch {\n throw new ServerAuthError(\"SIWE message is missing a valid address on line 2\");\n }\n\n const nonceMatch = message.match(/^Nonce: (.+)$/m);\n if (!nonceMatch || nonceMatch[1] === undefined) {\n throw new ServerAuthError(\"SIWE message is missing a Nonce line\");\n }\n const nonce = nonceMatch[1].trim();\n\n let recovered: string;\n try {\n recovered = await recoverMessageAddress({ message, signature: signature as Hex });\n } catch (error) {\n throw new ServerAuthError(\n `SIWE signature recovery failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n if (getAddress(recovered) !== claimed) {\n throw new ServerAuthError(\n `SIWE signature does not match message address (recovered ${recovered}, expected ${claimed})`,\n );\n }\n\n return { address: claimed, nonce };\n}\n\nexport interface SessionToken {\n token: string;\n expiresIn: number;\n}\n\nexport interface SessionClaims {\n sub: string;\n address: string;\n iat: number;\n exp: number;\n}\n\nexport function issueSessionToken(\n address: string,\n secret: string,\n ttlSeconds = DEFAULT_SESSION_TTL_SECONDS,\n): SessionToken {\n const normalized = getAddress(address);\n const now = Math.floor(Date.now() / 1000);\n const claims: SessionClaims = {\n sub: normalized,\n address: normalized,\n iat: now,\n exp: now + ttlSeconds,\n };\n return {\n token: signJwt({ alg: \"HS256\", typ: \"JWT\" }, claims, secret),\n expiresIn: ttlSeconds,\n };\n}\n\nexport function verifySessionToken(token: string, secret: string): { address: string } {\n const claims = verifyJwt(token, secret);\n if (typeof claims.sub !== \"string\" || claims.sub === \"\") {\n throw new ServerAuthError(\"session token missing 'sub' claim\");\n }\n return { address: claims.sub };\n}\n\nexport interface CreateSiweSessionOptions {\n jwtSecret: string;\n nonceStore?: NonceStore;\n sessionTtlSeconds?: number;\n}\n\nexport function createSiweSession(options: CreateSiweSessionOptions) {\n const nonceStore = options.nonceStore ?? new NonceStore();\n const sessionTtlSeconds = options.sessionTtlSeconds ?? DEFAULT_SESSION_TTL_SECONDS;\n\n return {\n issueNonce(address: string): string {\n return nonceStore.issue(address);\n },\n async verify(message: string, signature: string): Promise<SessionToken> {\n const verified = await verifySiweMessage(message, signature);\n if (!nonceStore.validate(verified.address, verified.nonce)) {\n throw new ServerAuthError(\"nonce is invalid, expired, or already used\");\n }\n return issueSessionToken(verified.address, options.jwtSecret, sessionTtlSeconds);\n },\n verifyToken(token: string): { address: string } {\n return verifySessionToken(token, options.jwtSecret);\n },\n };\n}\n\nfunction signJwt(header: object, payload: object, secret: string): string {\n const encodedHeader = base64UrlEncode(JSON.stringify(header));\n const encodedPayload = base64UrlEncode(JSON.stringify(payload));\n const signingInput = `${encodedHeader}.${encodedPayload}`;\n const signature = createHmac(\"sha256\", secret).update(signingInput).digest();\n return `${signingInput}.${base64UrlEncode(signature)}`;\n}\n\nfunction verifyJwt(token: string, secret: string): SessionClaims {\n const parts = token.split(\".\");\n if (parts.length !== 3 || !parts[0] || !parts[1] || !parts[2]) {\n throw new ServerAuthError(\"session token is not a valid JWT\");\n }\n\n const signingInput = `${parts[0]}.${parts[1]}`;\n const expected = createHmac(\"sha256\", secret).update(signingInput).digest();\n const actual = base64UrlDecode(parts[2]);\n if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) {\n throw new ServerAuthError(\"session token signature verification failed\");\n }\n\n const claims = JSON.parse(base64UrlDecode(parts[1]).toString(\"utf8\")) as SessionClaims;\n if (typeof claims.exp !== \"number\" || claims.exp <= Math.floor(Date.now() / 1000)) {\n throw new ServerAuthError(\"session token expired\");\n }\n return claims;\n}\n\nfunction base64UrlEncode(value: string | Buffer): string {\n const buffer = typeof value === \"string\" ? Buffer.from(value, \"utf8\") : value;\n return buffer.toString(\"base64url\");\n}\n\nfunction base64UrlDecode(value: string): Buffer {\n return Buffer.from(value, \"base64url\");\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tinycloud/server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0-beta.11",
|
|
4
4
|
"description": "Reusable TinyCloud server and agent integration helpers",
|
|
5
5
|
"author": "TinyCloud, Inc.",
|
|
6
6
|
"license": "EGPL",
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"test": "bun test src/"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@tinycloud/node-sdk": "3.1.0-beta.
|
|
32
|
-
"@tinycloud/sdk-core": "3.1.0-beta.
|
|
31
|
+
"@tinycloud/node-sdk": "3.1.0-beta.11",
|
|
32
|
+
"@tinycloud/sdk-core": "3.1.0-beta.11",
|
|
33
33
|
"viem": "^2.21.53"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|