@tinycloud/node-sdk 3.12.0-beta.1 → 3.12.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +39 -0
  2. package/dist/{NodeUserAuthorization-BtoDI9in.d.cts → NodeUserAuthorization-B_TFUlua.d.cts} +19 -53
  3. package/dist/{NodeUserAuthorization-BtoDI9in.d.ts → NodeUserAuthorization-B_TFUlua.d.ts} +19 -53
  4. package/dist/{TinyCloudNode-DLrINr98.d.ts → TinyCloudNode-Brcu8QoX.d.cts} +33 -59
  5. package/dist/{TinyCloudNode-C24728N_.d.cts → TinyCloudNode-DBJUnsMN.d.ts} +33 -59
  6. package/dist/{authority-BQN2Z3st.d.ts → authority-BFc5lesl.d.ts} +1 -1
  7. package/dist/{authority-DDdheOL6.d.cts → authority-DQ6R1HEG.d.cts} +1 -1
  8. package/dist/core.cjs +555 -320
  9. package/dist/core.cjs.map +1 -1
  10. package/dist/core.d.cts +5 -5
  11. package/dist/core.d.ts +5 -5
  12. package/dist/core.js +581 -324
  13. package/dist/core.js.map +1 -1
  14. package/dist/credentials-core.d.cts +1 -1
  15. package/dist/credentials-core.d.ts +1 -1
  16. package/dist/credentials.d.cts +1 -1
  17. package/dist/credentials.d.ts +1 -1
  18. package/dist/{delegation-BQbAuKad.d.cts → delegation-CeOWDnBB.d.cts} +7 -0
  19. package/dist/{delegation-BQbAuKad.d.ts → delegation-CeOWDnBB.d.ts} +7 -0
  20. package/dist/encryption-keys.d.cts +3 -3
  21. package/dist/encryption-keys.d.ts +3 -3
  22. package/dist/index.cjs +572 -337
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.d.cts +5 -4
  25. package/dist/index.d.ts +5 -4
  26. package/dist/index.js +582 -325
  27. package/dist/index.js.map +1 -1
  28. package/dist/replication/authority.d.cts +4 -4
  29. package/dist/replication/authority.d.ts +4 -4
  30. package/dist/space-hosting.cjs +48 -6
  31. package/dist/space-hosting.cjs.map +1 -1
  32. package/dist/space-hosting.d.cts +23 -8
  33. package/dist/space-hosting.d.ts +23 -8
  34. package/dist/space-hosting.js +48 -6
  35. package/dist/space-hosting.js.map +1 -1
  36. package/package.json +4 -4
@@ -1,9 +1,9 @@
1
1
  import '@tinycloud/sdk-core';
2
2
  import '@tinycloud/sdk-services';
3
3
  import '@tinycloud/sdk-services/kv/replication';
4
- import '../TinyCloudNode-C24728N_.cjs';
5
- export { A as AUTHORITY_EXPIRY_MARGIN_MS, D as DelegationPlanResult, R as ReplicaLocalEraseReport, a as ReplicationAuthorityHost, b as ReplicationAuthoritySession, c as assertRenewalParent, d as assertValidReplicationConfig, e as augmentSignInEntriesWithReplication, f as createReplicationAuthority, g as eraseReplicaLocalCopy, h as establishReplicaSessionStamp, i as hasUnrestrictedGetCoverage, m as mintNodeReplicationGrant, r as replicationScopeFromSignedSession, j as reportReplicaDenial, k as restoreReplicaSessionStamp, u as ucanAttCovers, l as ucanAttUnconstrainedFor, v as verifiedOwnerConsentAnchor } from '../authority-DDdheOL6.cjs';
4
+ import '../TinyCloudNode-Brcu8QoX.cjs';
5
+ export { A as AUTHORITY_EXPIRY_MARGIN_MS, D as DelegationPlanResult, R as ReplicaLocalEraseReport, a as ReplicationAuthorityHost, b as ReplicationAuthoritySession, c as assertRenewalParent, d as assertValidReplicationConfig, e as augmentSignInEntriesWithReplication, f as createReplicationAuthority, g as eraseReplicaLocalCopy, h as establishReplicaSessionStamp, i as hasUnrestrictedGetCoverage, m as mintNodeReplicationGrant, r as replicationScopeFromSignedSession, j as reportReplicaDenial, k as restoreReplicaSessionStamp, u as ucanAttCovers, l as ucanAttUnconstrainedFor, v as verifiedOwnerConsentAnchor } from '../authority-DQ6R1HEG.cjs';
6
6
  import '@tinycloud/sdk-services/encryption/named-keys';
7
- import '../NodeUserAuthorization-BtoDI9in.cjs';
7
+ import '../NodeUserAuthorization-B_TFUlua.cjs';
8
8
  import 'events';
9
- import '../delegation-BQbAuKad.cjs';
9
+ import '../delegation-CeOWDnBB.cjs';
@@ -1,9 +1,9 @@
1
1
  import '@tinycloud/sdk-core';
2
2
  import '@tinycloud/sdk-services';
3
3
  import '@tinycloud/sdk-services/kv/replication';
4
- import '../TinyCloudNode-DLrINr98.js';
5
- export { A as AUTHORITY_EXPIRY_MARGIN_MS, D as DelegationPlanResult, R as ReplicaLocalEraseReport, a as ReplicationAuthorityHost, b as ReplicationAuthoritySession, c as assertRenewalParent, d as assertValidReplicationConfig, e as augmentSignInEntriesWithReplication, f as createReplicationAuthority, g as eraseReplicaLocalCopy, h as establishReplicaSessionStamp, i as hasUnrestrictedGetCoverage, m as mintNodeReplicationGrant, r as replicationScopeFromSignedSession, j as reportReplicaDenial, k as restoreReplicaSessionStamp, u as ucanAttCovers, l as ucanAttUnconstrainedFor, v as verifiedOwnerConsentAnchor } from '../authority-BQN2Z3st.js';
4
+ import '../TinyCloudNode-DBJUnsMN.js';
5
+ export { A as AUTHORITY_EXPIRY_MARGIN_MS, D as DelegationPlanResult, R as ReplicaLocalEraseReport, a as ReplicationAuthorityHost, b as ReplicationAuthoritySession, c as assertRenewalParent, d as assertValidReplicationConfig, e as augmentSignInEntriesWithReplication, f as createReplicationAuthority, g as eraseReplicaLocalCopy, h as establishReplicaSessionStamp, i as hasUnrestrictedGetCoverage, m as mintNodeReplicationGrant, r as replicationScopeFromSignedSession, j as reportReplicaDenial, k as restoreReplicaSessionStamp, u as ucanAttCovers, l as ucanAttUnconstrainedFor, v as verifiedOwnerConsentAnchor } from '../authority-BFc5lesl.js';
6
6
  import '@tinycloud/sdk-services/encryption/named-keys';
7
- import '../NodeUserAuthorization-BtoDI9in.js';
7
+ import '../NodeUserAuthorization-B_TFUlua.js';
8
8
  import 'events';
9
- import '../delegation-BQbAuKad.js';
9
+ import '../delegation-CeOWDnBB.js';
@@ -105,11 +105,50 @@ function describeFailure(result) {
105
105
  const text = result.error?.trim();
106
106
  return text ? `${result.status} - ${text}` : `${result.status}`;
107
107
  }
108
- async function createPrimarySpace({ handler, context, guard, host, onHosted, activate }) {
109
- await guard();
110
- if (!await handler.confirmSpaceCreation(context)) return;
108
+ async function createPrimarySpace({
109
+ handler,
110
+ session,
111
+ context: existingContext,
112
+ currentSession,
113
+ signerAddress,
114
+ resolveHost,
115
+ domain,
116
+ hostName,
117
+ guard,
118
+ host,
119
+ onHosted,
120
+ activate,
121
+ signal
122
+ }) {
123
+ const checkCurrent = () => {
124
+ signal?.throwIfAborted();
125
+ if (session && currentSession?.() !== session) throw new Error("Space hosting session changed");
126
+ };
127
+ checkCurrent();
128
+ if (session && signerAddress && (await signerAddress()).toLowerCase() !== session.address.toLowerCase()) return;
129
+ checkCurrent();
130
+ const context = existingContext ?? (session && {
131
+ address: session.address,
132
+ chainId: session.chainId,
133
+ spaceId: session.spaceId,
134
+ host: await resolveHost?.() ?? hostName ?? ""
135
+ });
136
+ if (!context) throw new Error("Space hosting requires a session context");
137
+ checkCurrent();
138
+ await guard(context);
139
+ checkCurrent();
140
+ const approved = await handler.confirmSpaceCreation(context);
141
+ checkCurrent();
142
+ if (!approved) return;
111
143
  try {
112
- const created = await host();
144
+ const created = await host(context, checkCurrent, (peerId) => ({
145
+ address: context.address,
146
+ chainId: context.chainId,
147
+ domain: domain ?? "",
148
+ issuedAt: (/* @__PURE__ */ new Date()).toISOString(),
149
+ spaceId: context.spaceId,
150
+ peerId
151
+ }));
113
152
  if (!created.success) {
114
153
  throw Object.assign(
115
154
  new Error(`Failed to create space ${context.spaceId}: ${describeFailure(created)}`),
@@ -120,9 +159,12 @@ async function createPrimarySpace({ handler, context, guard, host, onHosted, act
120
159
  handler.onSpaceCreationFailed?.(context, error instanceof Error ? error : new Error(String(error)));
121
160
  throw error;
122
161
  }
123
- onHosted();
162
+ checkCurrent();
163
+ onHosted(context);
124
164
  await new Promise((resolve) => setTimeout(resolve, 100));
125
- const retry = await activate();
165
+ checkCurrent();
166
+ const retry = await activate(context);
167
+ checkCurrent();
126
168
  if (!retry.success) {
127
169
  const err = Object.assign(
128
170
  new Error(`Failed to activate session after creating space: ${describeFailure(retry)}`),
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/space-hosting.ts"],"sourcesContent":["/**\n * The hosting probe's read selection and classification, and the sequence that\n * creates a primary space that is not hosted. Both are loaded on first use, so\n * browser bundles carry them as a separate file rather than in their root.\n */\nimport type { ISpaceCreationHandler, SpaceCreationContext, SpaceHostResult } from \"@tinycloud/sdk-core\";\nimport type { FetchFunction, InvokeAnyEntry, ServiceHeaders } from \"@tinycloud/sdk-services\";\nimport type { SpaceHosting } from \"./authorization/NodeUserAuthorization\";\n\n/** A permission the session holds, as `TinyCloudNode` tracks it. */\nexport interface SpaceHostingOperation {\n spaceId?: string;\n resource?: string;\n service: string;\n path: string;\n action: string;\n caveats?: Record<string, unknown>[];\n}\n\n/** One read to send: `service`, `path` and `action` are what the invocation is signed for. */\nexport interface SpaceHostingRead {\n service: string;\n path: string;\n action: string;\n headers?: Record<string, string>;\n body?: string;\n}\n\nexport interface SpaceHostingPorts {\n spaceId: string;\n /** Everything the session holds, on any space: its own recap and installed runtime grants. */\n operations: readonly SpaceHostingOperation[];\n /** Whether `granted` authorizes `requested`. */\n covers(granted: SpaceHostingOperation, requested: SpaceHostingOperation): boolean;\n /** Identifies the session's services: `hosted` answers are remembered per scope, so a later session never inherits one. */\n scope: object;\n host: string;\n fetch: FetchFunction;\n /** Sign an invocation for exactly these entries. `invoke` would always sign for the session's primary space. */\n sign(entries: InvokeAnyEntry[]): ServiceHeaders;\n /** Register the session delegation with the node. Rejects on a network failure or timeout. */\n activate(signal: AbortSignal): Promise<unknown>;\n}\n\n/** Spaces the node answered `hosted` for, by session: hosting does not go away in normal operation. */\nconst hostedBy = new WeakMap<object, Set<string>>();\n\n/** Per request: the probe never holds up a caller longer. */\nconst TIMEOUT_MS = 5_000;\n\n/** A key that does not exist: a hosted space answers a read of it with a plain 404. */\nconst SPACE_PROBE_KEY = \".tinycloud-space-probe\";\nconst KV_LIST_ACTION = \"tinycloud.kv/list\";\nconst KV_METADATA_ACTION = \"tinycloud.kv/metadata\";\nconst KV_GET_ACTION = \"tinycloud.kv/get\";\nconst SPACE_INFO_ACTION = \"tinycloud.space/info\";\n\nconst isPrefix = (path: string) =>\n path === \"\" || path.endsWith(\"/\") || path.endsWith(\"/*\") || path.endsWith(\"/**\");\nconst prefixOf = (path: string) =>\n path.endsWith(\"/**\") ? path.slice(0, -2) : path.endsWith(\"/*\") ? path.slice(0, -1) : path === \"/\" ? \"\" : path;\n// A prefix grant is read at a key that does not exist; a grant on one key reads that key.\nconst keyOf = (path: string) => isPrefix(path) ? `${prefixOf(path)}${SPACE_PROBE_KEY}` : path;\n\n/**\n * The cheapest read of the space the session already holds: never one that\n * downloads a body when another exists. Reads under a caveated grant are\n * skipped, since their limits are not known here.\n */\nfunction selectRead({ spaceId, operations, covers }: SpaceHostingPorts): SpaceHostingRead | undefined {\n // Held on this space: `covers` an operation's own action and path only when it names this space and no resource.\n const held = operations.filter((operation) =>\n !operation.caveats?.length &&\n covers(operation, { spaceId, service: operation.service, path: operation.path, action: operation.action }));\n const holding = (service: string, action: string) =>\n held.filter((operation) => covers(operation, { spaceId, service, path: operation.path, action }));\n\n return [\n ...holding(\"kv\", KV_LIST_ACTION).map((operation) => ({\n service: \"kv\",\n path: isPrefix(operation.path) ? prefixOf(operation.path) : operation.path,\n action: KV_LIST_ACTION,\n headers: { \"x-tinycloud-limit\": \"1\" },\n })),\n ...holding(\"kv\", KV_METADATA_ACTION).map((operation) => ({\n service: \"kv\", path: keyOf(operation.path), action: KV_METADATA_ACTION,\n })),\n ...holding(\"space\", SPACE_INFO_ACTION).map(() => ({\n service: \"space\", path: \"\", action: SPACE_INFO_ACTION, body: \"{}\",\n })),\n ...holding(\"kv\", KV_GET_ACTION).map((operation) => ({\n service: \"kv\", path: keyOf(operation.path), action: KV_GET_ACTION,\n })),\n ].find((read) => held.some((operation) =>\n covers(operation, { spaceId, service: read.service, path: read.path, action: read.action })));\n}\n\n/** One signed read, classified. */\nasync function send(ports: SpaceHostingPorts, read: SpaceHostingRead): Promise<SpaceHosting | \"refused\"> {\n try {\n const { spaceId } = ports;\n const response = await ports.fetch(`${ports.host}/invoke`, {\n method: \"POST\",\n headers: { ...ports.sign([{ spaceId, service: read.service, path: read.path, action: read.action }]), ...read.headers },\n body: read.body,\n signal: AbortSignal.timeout(TIMEOUT_MS),\n });\n if (response.ok) {\n // A 2xx is the answer; a body that comes with it is not wanted.\n if (response.body instanceof ReadableStream) await response.body.cancel().catch(() => undefined);\n return \"hosted\";\n }\n if (response.status === 401 || response.status === 403) return \"refused\";\n if (response.status === 404) {\n // Only the node's unhosted-space 404 says \"not hosted\"; a missing key is a 404 too.\n return /space not found/i.test(await response.text()) ? \"not-hosted\" : \"hosted\";\n }\n return \"unknown\";\n } catch {\n return \"unknown\";\n }\n}\n\n/**\n * Ask the node whether the space is hosted. A 401/403 registers the session\n * delegation once and asks once more; still refused is `unknown`, as are\n * network failures, 5xx and a session that holds no read.\n */\nexport async function askSpaceHosting(ports: SpaceHostingPorts): Promise<SpaceHosting> {\n const read = selectRead(ports);\n if (!read) return \"unknown\";\n // Only a session that holds a read on the space is answered from what it already saw.\n // The address in a space id is case-insensitive; the rest of it is not.\n const key = `${ports.host} ${ports.spaceId.replace(/^(tinycloud:pkh:eip155:\\d+:)(0x[0-9a-fA-F]{40})/, (_, prefix: string, address: string) => prefix + address.toLowerCase())}`;\n if (hostedBy.get(ports.scope)?.has(key)) return \"hosted\";\n let outcome = await send(ports, read);\n if (outcome === \"refused\") {\n try {\n await ports.activate(AbortSignal.timeout(TIMEOUT_MS));\n } catch {\n return \"unknown\";\n }\n outcome = await send(ports, read);\n }\n if (outcome === \"hosted\") hostedBy.set(ports.scope, (hostedBy.get(ports.scope) ?? new Set<string>()).add(key));\n return outcome === \"refused\" ? \"unknown\" : outcome;\n}\n\nexport interface PrimarySpaceCreationPorts {\n handler: ISpaceCreationHandler;\n context: SpaceCreationContext;\n /** `assertMayCreateSpace`, reporting a refusal to the handler. */\n guard(): Promise<void>;\n /** Submit the host delegation for the primary space. */\n host(): Promise<SpaceHostResult>;\n /** Called once the node has the host delegation. */\n onHosted(): void;\n /** Register the session delegation again. */\n activate(): Promise<SpaceHostResult>;\n}\n\n/** `<status> - <server text>` for a failed host call. */\nfunction describeFailure(result: SpaceHostResult): string {\n const text = result.error?.trim();\n return text ? `${result.status} - ${text}` : `${result.status}`;\n}\n\n/**\n * The guard, confirm, host and re-activate sequence for an unhosted primary\n * space. A declined confirmation ends it quietly; a failure is reported to the\n * handler and thrown.\n */\nexport async function createPrimarySpace({ handler, context, guard, host, onHosted, activate }: PrimarySpaceCreationPorts): Promise<void> {\n await guard();\n if (!await handler.confirmSpaceCreation(context)) return;\n\n try {\n const created = await host();\n if (!created.success) {\n throw Object.assign(\n new Error(`Failed to create space ${context.spaceId}: ${describeFailure(created)}`),\n { cause: created },\n );\n }\n } catch (error) {\n handler.onSpaceCreationFailed?.(context, error instanceof Error ? error : new Error(String(error)));\n throw error;\n }\n onHosted();\n\n // Small delay to allow space creation to propagate\n await new Promise((resolve) => setTimeout(resolve, 100));\n\n const retry = await activate();\n if (!retry.success) {\n const err = Object.assign(\n new Error(`Failed to activate session after creating space: ${describeFailure(retry)}`),\n { cause: retry },\n );\n handler.onSpaceCreationFailed?.(context, err);\n throw err;\n }\n handler.onSpaceCreated?.(context);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/space-hosting.ts"],"sourcesContent":["/**\n * The hosting probe's read selection and classification, and the sequence that\n * creates a primary space that is not hosted. Both are loaded on first use, so\n * browser bundles carry them as a separate file rather than in their root.\n */\nimport type { ISpaceCreationHandler, SpaceCreationContext, SpaceHostResult } from \"@tinycloud/sdk-core\";\nimport type { FetchFunction, InvokeAnyEntry, ServiceHeaders } from \"@tinycloud/sdk-services\";\nimport type { SpaceHosting } from \"./authorization/NodeUserAuthorization\";\n\n/** A permission the session holds, as `TinyCloudNode` tracks it. */\nexport interface SpaceHostingOperation {\n spaceId?: string;\n resource?: string;\n service: string;\n path: string;\n action: string;\n caveats?: Record<string, unknown>[];\n}\n\n/** One read to send: `service`, `path` and `action` are what the invocation is signed for. */\nexport interface SpaceHostingRead {\n service: string;\n path: string;\n action: string;\n headers?: Record<string, string>;\n body?: string;\n}\n\nexport interface SpaceHostingPorts {\n spaceId: string;\n /** Everything the session holds, on any space: its own recap and installed runtime grants. */\n operations: readonly SpaceHostingOperation[];\n /** Whether `granted` authorizes `requested`. */\n covers(granted: SpaceHostingOperation, requested: SpaceHostingOperation): boolean;\n /** Identifies the session's services: `hosted` answers are remembered per scope, so a later session never inherits one. */\n scope: object;\n host: string;\n fetch: FetchFunction;\n /** Sign an invocation for exactly these entries. `invoke` would always sign for the session's primary space. */\n sign(entries: InvokeAnyEntry[]): ServiceHeaders;\n /** Register the session delegation with the node. Rejects on a network failure or timeout. */\n activate(signal: AbortSignal): Promise<unknown>;\n}\n\n/** Spaces the node answered `hosted` for, by session: hosting does not go away in normal operation. */\nconst hostedBy = new WeakMap<object, Set<string>>();\n\n/** Per request: the probe never holds up a caller longer. */\nconst TIMEOUT_MS = 5_000;\n\n/** A key that does not exist: a hosted space answers a read of it with a plain 404. */\nconst SPACE_PROBE_KEY = \".tinycloud-space-probe\";\nconst KV_LIST_ACTION = \"tinycloud.kv/list\";\nconst KV_METADATA_ACTION = \"tinycloud.kv/metadata\";\nconst KV_GET_ACTION = \"tinycloud.kv/get\";\nconst SPACE_INFO_ACTION = \"tinycloud.space/info\";\n\nconst isPrefix = (path: string) =>\n path === \"\" || path.endsWith(\"/\") || path.endsWith(\"/*\") || path.endsWith(\"/**\");\nconst prefixOf = (path: string) =>\n path.endsWith(\"/**\") ? path.slice(0, -2) : path.endsWith(\"/*\") ? path.slice(0, -1) : path === \"/\" ? \"\" : path;\n// A prefix grant is read at a key that does not exist; a grant on one key reads that key.\nconst keyOf = (path: string) => isPrefix(path) ? `${prefixOf(path)}${SPACE_PROBE_KEY}` : path;\n\n/**\n * The cheapest read of the space the session already holds: never one that\n * downloads a body when another exists. Reads under a caveated grant are\n * skipped, since their limits are not known here.\n */\nfunction selectRead({ spaceId, operations, covers }: SpaceHostingPorts): SpaceHostingRead | undefined {\n // Held on this space: `covers` an operation's own action and path only when it names this space and no resource.\n const held = operations.filter((operation) =>\n !operation.caveats?.length &&\n covers(operation, { spaceId, service: operation.service, path: operation.path, action: operation.action }));\n const holding = (service: string, action: string) =>\n held.filter((operation) => covers(operation, { spaceId, service, path: operation.path, action }));\n\n return [\n ...holding(\"kv\", KV_LIST_ACTION).map((operation) => ({\n service: \"kv\",\n path: isPrefix(operation.path) ? prefixOf(operation.path) : operation.path,\n action: KV_LIST_ACTION,\n headers: { \"x-tinycloud-limit\": \"1\" },\n })),\n ...holding(\"kv\", KV_METADATA_ACTION).map((operation) => ({\n service: \"kv\", path: keyOf(operation.path), action: KV_METADATA_ACTION,\n })),\n ...holding(\"space\", SPACE_INFO_ACTION).map(() => ({\n service: \"space\", path: \"\", action: SPACE_INFO_ACTION, body: \"{}\",\n })),\n ...holding(\"kv\", KV_GET_ACTION).map((operation) => ({\n service: \"kv\", path: keyOf(operation.path), action: KV_GET_ACTION,\n })),\n ].find((read) => held.some((operation) =>\n covers(operation, { spaceId, service: read.service, path: read.path, action: read.action })));\n}\n\n/** One signed read, classified. */\nasync function send(ports: SpaceHostingPorts, read: SpaceHostingRead): Promise<SpaceHosting | \"refused\"> {\n try {\n const { spaceId } = ports;\n const response = await ports.fetch(`${ports.host}/invoke`, {\n method: \"POST\",\n headers: { ...ports.sign([{ spaceId, service: read.service, path: read.path, action: read.action }]), ...read.headers },\n body: read.body,\n signal: AbortSignal.timeout(TIMEOUT_MS),\n });\n if (response.ok) {\n // A 2xx is the answer; a body that comes with it is not wanted.\n if (response.body instanceof ReadableStream) await response.body.cancel().catch(() => undefined);\n return \"hosted\";\n }\n if (response.status === 401 || response.status === 403) return \"refused\";\n if (response.status === 404) {\n // Only the node's unhosted-space 404 says \"not hosted\"; a missing key is a 404 too.\n return /space not found/i.test(await response.text()) ? \"not-hosted\" : \"hosted\";\n }\n return \"unknown\";\n } catch {\n return \"unknown\";\n }\n}\n\n/**\n * Ask the node whether the space is hosted. A 401/403 registers the session\n * delegation once and asks once more; still refused is `unknown`, as are\n * network failures, 5xx and a session that holds no read.\n */\nexport async function askSpaceHosting(ports: SpaceHostingPorts): Promise<SpaceHosting> {\n const read = selectRead(ports);\n if (!read) return \"unknown\";\n // Only a session that holds a read on the space is answered from what it already saw.\n // The address in a space id is case-insensitive; the rest of it is not.\n const key = `${ports.host} ${ports.spaceId.replace(/^(tinycloud:pkh:eip155:\\d+:)(0x[0-9a-fA-F]{40})/, (_, prefix: string, address: string) => prefix + address.toLowerCase())}`;\n if (hostedBy.get(ports.scope)?.has(key)) return \"hosted\";\n let outcome = await send(ports, read);\n if (outcome === \"refused\") {\n try {\n await ports.activate(AbortSignal.timeout(TIMEOUT_MS));\n } catch {\n return \"unknown\";\n }\n outcome = await send(ports, read);\n }\n if (outcome === \"hosted\") hostedBy.set(ports.scope, (hostedBy.get(ports.scope) ?? new Set<string>()).add(key));\n return outcome === \"refused\" ? \"unknown\" : outcome;\n}\n\ntype PrimarySpaceIdentity = Pick<SpaceCreationContext, \"address\" | \"chainId\" | \"spaceId\">;\n\nexport interface PrimarySpaceCreationPorts {\n handler: ISpaceCreationHandler;\n session?: PrimarySpaceIdentity;\n context?: SpaceCreationContext;\n currentSession?(): unknown;\n signerAddress?(): Promise<string>;\n resolveHost?(): Promise<string>;\n domain?: string;\n hostName?: string;\n /** `assertMayCreateSpace`, reporting a refusal to the handler. */\n guard(context: SpaceCreationContext): Promise<void>;\n /** Confirm that the host delegation should be submitted. */\n host(\n context: SpaceCreationContext,\n assertCurrent: () => void,\n siweParameters: (peerId: string) => {\n address: string;\n chainId: number;\n domain: string;\n issuedAt: string;\n spaceId: string;\n peerId: string;\n },\n ): Promise<SpaceHostResult>;\n /** Called once the node has the host delegation. */\n onHosted(context: SpaceCreationContext): void;\n /** Register the session delegation again. */\n activate(context: SpaceCreationContext): Promise<SpaceHostResult>;\n signal?: AbortSignal;\n}\n/** `<status> - <server text>` for a failed host call. */\nfunction describeFailure(result: SpaceHostResult): string {\n const text = result.error?.trim();\n return text ? `${result.status} - ${text}` : `${result.status}`;\n}\n\n/**\n * The guard, confirm, host and re-activate sequence for an unhosted primary\n * space. A declined confirmation ends it quietly; a failure is reported to the\n * handler and thrown.\n */\nexport async function createPrimarySpace({\n handler,\n session,\n context: existingContext,\n currentSession,\n signerAddress,\n resolveHost,\n domain,\n hostName,\n guard,\n host,\n onHosted,\n activate,\n signal,\n}: PrimarySpaceCreationPorts): Promise<void> {\n const checkCurrent = () => {\n signal?.throwIfAborted();\n if (session && currentSession?.() !== session) throw new Error(\"Space hosting session changed\");\n };\n checkCurrent();\n if (session && signerAddress && (await signerAddress()).toLowerCase() !== session.address.toLowerCase()) return;\n checkCurrent();\n const context = existingContext ?? (session && {\n address: session.address,\n chainId: session.chainId,\n spaceId: session.spaceId,\n host: await resolveHost?.() ?? hostName ?? \"\",\n });\n if (!context) throw new Error(\"Space hosting requires a session context\");\n checkCurrent();\n await guard(context);\n checkCurrent();\n const approved = await handler.confirmSpaceCreation(context);\n checkCurrent();\n if (!approved) return;\n\n try {\n const created = await host(context, checkCurrent, (peerId) => ({\n address: context.address,\n chainId: context.chainId,\n domain: domain ?? \"\",\n issuedAt: new Date().toISOString(),\n spaceId: context.spaceId,\n peerId,\n }));\n if (!created.success) {\n throw Object.assign(\n new Error(`Failed to create space ${context.spaceId}: ${describeFailure(created)}`),\n { cause: created },\n );\n }\n } catch (error) {\n handler.onSpaceCreationFailed?.(context, error instanceof Error ? error : new Error(String(error)));\n throw error;\n }\n checkCurrent();\n onHosted(context);\n\n // Small delay to allow space creation to propagate\n await new Promise((resolve) => setTimeout(resolve, 100));\n checkCurrent();\n\n const retry = await activate(context);\n checkCurrent();\n if (!retry.success) {\n const err = Object.assign(\n new Error(`Failed to activate session after creating space: ${describeFailure(retry)}`),\n { cause: retry },\n );\n handler.onSpaceCreationFailed?.(context, err);\n throw err;\n }\n handler.onSpaceCreated?.(context);\n}\n"],"mappings":"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","names":[]}
@@ -1,6 +1,6 @@
1
1
  import { ISpaceCreationHandler, SpaceCreationContext, SpaceHostResult } from '@tinycloud/sdk-core';
2
2
  import { FetchFunction, InvokeAnyEntry, ServiceHeaders } from '@tinycloud/sdk-services';
3
- import { S as SpaceHosting } from './NodeUserAuthorization-BtoDI9in.cjs';
3
+ import { S as SpaceHosting } from './NodeUserAuthorization-B_TFUlua.cjs';
4
4
  import 'events';
5
5
 
6
6
  /**
@@ -47,23 +47,38 @@ interface SpaceHostingPorts {
47
47
  * network failures, 5xx and a session that holds no read.
48
48
  */
49
49
  declare function askSpaceHosting(ports: SpaceHostingPorts): Promise<SpaceHosting>;
50
+ type PrimarySpaceIdentity = Pick<SpaceCreationContext, "address" | "chainId" | "spaceId">;
50
51
  interface PrimarySpaceCreationPorts {
51
52
  handler: ISpaceCreationHandler;
52
- context: SpaceCreationContext;
53
+ session?: PrimarySpaceIdentity;
54
+ context?: SpaceCreationContext;
55
+ currentSession?(): unknown;
56
+ signerAddress?(): Promise<string>;
57
+ resolveHost?(): Promise<string>;
58
+ domain?: string;
59
+ hostName?: string;
53
60
  /** `assertMayCreateSpace`, reporting a refusal to the handler. */
54
- guard(): Promise<void>;
55
- /** Submit the host delegation for the primary space. */
56
- host(): Promise<SpaceHostResult>;
61
+ guard(context: SpaceCreationContext): Promise<void>;
62
+ /** Confirm that the host delegation should be submitted. */
63
+ host(context: SpaceCreationContext, assertCurrent: () => void, siweParameters: (peerId: string) => {
64
+ address: string;
65
+ chainId: number;
66
+ domain: string;
67
+ issuedAt: string;
68
+ spaceId: string;
69
+ peerId: string;
70
+ }): Promise<SpaceHostResult>;
57
71
  /** Called once the node has the host delegation. */
58
- onHosted(): void;
72
+ onHosted(context: SpaceCreationContext): void;
59
73
  /** Register the session delegation again. */
60
- activate(): Promise<SpaceHostResult>;
74
+ activate(context: SpaceCreationContext): Promise<SpaceHostResult>;
75
+ signal?: AbortSignal;
61
76
  }
62
77
  /**
63
78
  * The guard, confirm, host and re-activate sequence for an unhosted primary
64
79
  * space. A declined confirmation ends it quietly; a failure is reported to the
65
80
  * handler and thrown.
66
81
  */
67
- declare function createPrimarySpace({ handler, context, guard, host, onHosted, activate }: PrimarySpaceCreationPorts): Promise<void>;
82
+ declare function createPrimarySpace({ handler, session, context: existingContext, currentSession, signerAddress, resolveHost, domain, hostName, guard, host, onHosted, activate, signal, }: PrimarySpaceCreationPorts): Promise<void>;
68
83
 
69
84
  export { type PrimarySpaceCreationPorts, type SpaceHostingOperation, type SpaceHostingPorts, type SpaceHostingRead, askSpaceHosting, createPrimarySpace };
@@ -1,6 +1,6 @@
1
1
  import { ISpaceCreationHandler, SpaceCreationContext, SpaceHostResult } from '@tinycloud/sdk-core';
2
2
  import { FetchFunction, InvokeAnyEntry, ServiceHeaders } from '@tinycloud/sdk-services';
3
- import { S as SpaceHosting } from './NodeUserAuthorization-BtoDI9in.js';
3
+ import { S as SpaceHosting } from './NodeUserAuthorization-B_TFUlua.js';
4
4
  import 'events';
5
5
 
6
6
  /**
@@ -47,23 +47,38 @@ interface SpaceHostingPorts {
47
47
  * network failures, 5xx and a session that holds no read.
48
48
  */
49
49
  declare function askSpaceHosting(ports: SpaceHostingPorts): Promise<SpaceHosting>;
50
+ type PrimarySpaceIdentity = Pick<SpaceCreationContext, "address" | "chainId" | "spaceId">;
50
51
  interface PrimarySpaceCreationPorts {
51
52
  handler: ISpaceCreationHandler;
52
- context: SpaceCreationContext;
53
+ session?: PrimarySpaceIdentity;
54
+ context?: SpaceCreationContext;
55
+ currentSession?(): unknown;
56
+ signerAddress?(): Promise<string>;
57
+ resolveHost?(): Promise<string>;
58
+ domain?: string;
59
+ hostName?: string;
53
60
  /** `assertMayCreateSpace`, reporting a refusal to the handler. */
54
- guard(): Promise<void>;
55
- /** Submit the host delegation for the primary space. */
56
- host(): Promise<SpaceHostResult>;
61
+ guard(context: SpaceCreationContext): Promise<void>;
62
+ /** Confirm that the host delegation should be submitted. */
63
+ host(context: SpaceCreationContext, assertCurrent: () => void, siweParameters: (peerId: string) => {
64
+ address: string;
65
+ chainId: number;
66
+ domain: string;
67
+ issuedAt: string;
68
+ spaceId: string;
69
+ peerId: string;
70
+ }): Promise<SpaceHostResult>;
57
71
  /** Called once the node has the host delegation. */
58
- onHosted(): void;
72
+ onHosted(context: SpaceCreationContext): void;
59
73
  /** Register the session delegation again. */
60
- activate(): Promise<SpaceHostResult>;
74
+ activate(context: SpaceCreationContext): Promise<SpaceHostResult>;
75
+ signal?: AbortSignal;
61
76
  }
62
77
  /**
63
78
  * The guard, confirm, host and re-activate sequence for an unhosted primary
64
79
  * space. A declined confirmation ends it quietly; a failure is reported to the
65
80
  * handler and thrown.
66
81
  */
67
- declare function createPrimarySpace({ handler, context, guard, host, onHosted, activate }: PrimarySpaceCreationPorts): Promise<void>;
82
+ declare function createPrimarySpace({ handler, session, context: existingContext, currentSession, signerAddress, resolveHost, domain, hostName, guard, host, onHosted, activate, signal, }: PrimarySpaceCreationPorts): Promise<void>;
68
83
 
69
84
  export { type PrimarySpaceCreationPorts, type SpaceHostingOperation, type SpaceHostingPorts, type SpaceHostingRead, askSpaceHosting, createPrimarySpace };
@@ -80,11 +80,50 @@ function describeFailure(result) {
80
80
  const text = result.error?.trim();
81
81
  return text ? `${result.status} - ${text}` : `${result.status}`;
82
82
  }
83
- async function createPrimarySpace({ handler, context, guard, host, onHosted, activate }) {
84
- await guard();
85
- if (!await handler.confirmSpaceCreation(context)) return;
83
+ async function createPrimarySpace({
84
+ handler,
85
+ session,
86
+ context: existingContext,
87
+ currentSession,
88
+ signerAddress,
89
+ resolveHost,
90
+ domain,
91
+ hostName,
92
+ guard,
93
+ host,
94
+ onHosted,
95
+ activate,
96
+ signal
97
+ }) {
98
+ const checkCurrent = () => {
99
+ signal?.throwIfAborted();
100
+ if (session && currentSession?.() !== session) throw new Error("Space hosting session changed");
101
+ };
102
+ checkCurrent();
103
+ if (session && signerAddress && (await signerAddress()).toLowerCase() !== session.address.toLowerCase()) return;
104
+ checkCurrent();
105
+ const context = existingContext ?? (session && {
106
+ address: session.address,
107
+ chainId: session.chainId,
108
+ spaceId: session.spaceId,
109
+ host: await resolveHost?.() ?? hostName ?? ""
110
+ });
111
+ if (!context) throw new Error("Space hosting requires a session context");
112
+ checkCurrent();
113
+ await guard(context);
114
+ checkCurrent();
115
+ const approved = await handler.confirmSpaceCreation(context);
116
+ checkCurrent();
117
+ if (!approved) return;
86
118
  try {
87
- const created = await host();
119
+ const created = await host(context, checkCurrent, (peerId) => ({
120
+ address: context.address,
121
+ chainId: context.chainId,
122
+ domain: domain ?? "",
123
+ issuedAt: (/* @__PURE__ */ new Date()).toISOString(),
124
+ spaceId: context.spaceId,
125
+ peerId
126
+ }));
88
127
  if (!created.success) {
89
128
  throw Object.assign(
90
129
  new Error(`Failed to create space ${context.spaceId}: ${describeFailure(created)}`),
@@ -95,9 +134,12 @@ async function createPrimarySpace({ handler, context, guard, host, onHosted, act
95
134
  handler.onSpaceCreationFailed?.(context, error instanceof Error ? error : new Error(String(error)));
96
135
  throw error;
97
136
  }
98
- onHosted();
137
+ checkCurrent();
138
+ onHosted(context);
99
139
  await new Promise((resolve) => setTimeout(resolve, 100));
100
- const retry = await activate();
140
+ checkCurrent();
141
+ const retry = await activate(context);
142
+ checkCurrent();
101
143
  if (!retry.success) {
102
144
  const err = Object.assign(
103
145
  new Error(`Failed to activate session after creating space: ${describeFailure(retry)}`),
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/space-hosting.ts"],"sourcesContent":["/**\n * The hosting probe's read selection and classification, and the sequence that\n * creates a primary space that is not hosted. Both are loaded on first use, so\n * browser bundles carry them as a separate file rather than in their root.\n */\nimport type { ISpaceCreationHandler, SpaceCreationContext, SpaceHostResult } from \"@tinycloud/sdk-core\";\nimport type { FetchFunction, InvokeAnyEntry, ServiceHeaders } from \"@tinycloud/sdk-services\";\nimport type { SpaceHosting } from \"./authorization/NodeUserAuthorization\";\n\n/** A permission the session holds, as `TinyCloudNode` tracks it. */\nexport interface SpaceHostingOperation {\n spaceId?: string;\n resource?: string;\n service: string;\n path: string;\n action: string;\n caveats?: Record<string, unknown>[];\n}\n\n/** One read to send: `service`, `path` and `action` are what the invocation is signed for. */\nexport interface SpaceHostingRead {\n service: string;\n path: string;\n action: string;\n headers?: Record<string, string>;\n body?: string;\n}\n\nexport interface SpaceHostingPorts {\n spaceId: string;\n /** Everything the session holds, on any space: its own recap and installed runtime grants. */\n operations: readonly SpaceHostingOperation[];\n /** Whether `granted` authorizes `requested`. */\n covers(granted: SpaceHostingOperation, requested: SpaceHostingOperation): boolean;\n /** Identifies the session's services: `hosted` answers are remembered per scope, so a later session never inherits one. */\n scope: object;\n host: string;\n fetch: FetchFunction;\n /** Sign an invocation for exactly these entries. `invoke` would always sign for the session's primary space. */\n sign(entries: InvokeAnyEntry[]): ServiceHeaders;\n /** Register the session delegation with the node. Rejects on a network failure or timeout. */\n activate(signal: AbortSignal): Promise<unknown>;\n}\n\n/** Spaces the node answered `hosted` for, by session: hosting does not go away in normal operation. */\nconst hostedBy = new WeakMap<object, Set<string>>();\n\n/** Per request: the probe never holds up a caller longer. */\nconst TIMEOUT_MS = 5_000;\n\n/** A key that does not exist: a hosted space answers a read of it with a plain 404. */\nconst SPACE_PROBE_KEY = \".tinycloud-space-probe\";\nconst KV_LIST_ACTION = \"tinycloud.kv/list\";\nconst KV_METADATA_ACTION = \"tinycloud.kv/metadata\";\nconst KV_GET_ACTION = \"tinycloud.kv/get\";\nconst SPACE_INFO_ACTION = \"tinycloud.space/info\";\n\nconst isPrefix = (path: string) =>\n path === \"\" || path.endsWith(\"/\") || path.endsWith(\"/*\") || path.endsWith(\"/**\");\nconst prefixOf = (path: string) =>\n path.endsWith(\"/**\") ? path.slice(0, -2) : path.endsWith(\"/*\") ? path.slice(0, -1) : path === \"/\" ? \"\" : path;\n// A prefix grant is read at a key that does not exist; a grant on one key reads that key.\nconst keyOf = (path: string) => isPrefix(path) ? `${prefixOf(path)}${SPACE_PROBE_KEY}` : path;\n\n/**\n * The cheapest read of the space the session already holds: never one that\n * downloads a body when another exists. Reads under a caveated grant are\n * skipped, since their limits are not known here.\n */\nfunction selectRead({ spaceId, operations, covers }: SpaceHostingPorts): SpaceHostingRead | undefined {\n // Held on this space: `covers` an operation's own action and path only when it names this space and no resource.\n const held = operations.filter((operation) =>\n !operation.caveats?.length &&\n covers(operation, { spaceId, service: operation.service, path: operation.path, action: operation.action }));\n const holding = (service: string, action: string) =>\n held.filter((operation) => covers(operation, { spaceId, service, path: operation.path, action }));\n\n return [\n ...holding(\"kv\", KV_LIST_ACTION).map((operation) => ({\n service: \"kv\",\n path: isPrefix(operation.path) ? prefixOf(operation.path) : operation.path,\n action: KV_LIST_ACTION,\n headers: { \"x-tinycloud-limit\": \"1\" },\n })),\n ...holding(\"kv\", KV_METADATA_ACTION).map((operation) => ({\n service: \"kv\", path: keyOf(operation.path), action: KV_METADATA_ACTION,\n })),\n ...holding(\"space\", SPACE_INFO_ACTION).map(() => ({\n service: \"space\", path: \"\", action: SPACE_INFO_ACTION, body: \"{}\",\n })),\n ...holding(\"kv\", KV_GET_ACTION).map((operation) => ({\n service: \"kv\", path: keyOf(operation.path), action: KV_GET_ACTION,\n })),\n ].find((read) => held.some((operation) =>\n covers(operation, { spaceId, service: read.service, path: read.path, action: read.action })));\n}\n\n/** One signed read, classified. */\nasync function send(ports: SpaceHostingPorts, read: SpaceHostingRead): Promise<SpaceHosting | \"refused\"> {\n try {\n const { spaceId } = ports;\n const response = await ports.fetch(`${ports.host}/invoke`, {\n method: \"POST\",\n headers: { ...ports.sign([{ spaceId, service: read.service, path: read.path, action: read.action }]), ...read.headers },\n body: read.body,\n signal: AbortSignal.timeout(TIMEOUT_MS),\n });\n if (response.ok) {\n // A 2xx is the answer; a body that comes with it is not wanted.\n if (response.body instanceof ReadableStream) await response.body.cancel().catch(() => undefined);\n return \"hosted\";\n }\n if (response.status === 401 || response.status === 403) return \"refused\";\n if (response.status === 404) {\n // Only the node's unhosted-space 404 says \"not hosted\"; a missing key is a 404 too.\n return /space not found/i.test(await response.text()) ? \"not-hosted\" : \"hosted\";\n }\n return \"unknown\";\n } catch {\n return \"unknown\";\n }\n}\n\n/**\n * Ask the node whether the space is hosted. A 401/403 registers the session\n * delegation once and asks once more; still refused is `unknown`, as are\n * network failures, 5xx and a session that holds no read.\n */\nexport async function askSpaceHosting(ports: SpaceHostingPorts): Promise<SpaceHosting> {\n const read = selectRead(ports);\n if (!read) return \"unknown\";\n // Only a session that holds a read on the space is answered from what it already saw.\n // The address in a space id is case-insensitive; the rest of it is not.\n const key = `${ports.host} ${ports.spaceId.replace(/^(tinycloud:pkh:eip155:\\d+:)(0x[0-9a-fA-F]{40})/, (_, prefix: string, address: string) => prefix + address.toLowerCase())}`;\n if (hostedBy.get(ports.scope)?.has(key)) return \"hosted\";\n let outcome = await send(ports, read);\n if (outcome === \"refused\") {\n try {\n await ports.activate(AbortSignal.timeout(TIMEOUT_MS));\n } catch {\n return \"unknown\";\n }\n outcome = await send(ports, read);\n }\n if (outcome === \"hosted\") hostedBy.set(ports.scope, (hostedBy.get(ports.scope) ?? new Set<string>()).add(key));\n return outcome === \"refused\" ? \"unknown\" : outcome;\n}\n\nexport interface PrimarySpaceCreationPorts {\n handler: ISpaceCreationHandler;\n context: SpaceCreationContext;\n /** `assertMayCreateSpace`, reporting a refusal to the handler. */\n guard(): Promise<void>;\n /** Submit the host delegation for the primary space. */\n host(): Promise<SpaceHostResult>;\n /** Called once the node has the host delegation. */\n onHosted(): void;\n /** Register the session delegation again. */\n activate(): Promise<SpaceHostResult>;\n}\n\n/** `<status> - <server text>` for a failed host call. */\nfunction describeFailure(result: SpaceHostResult): string {\n const text = result.error?.trim();\n return text ? `${result.status} - ${text}` : `${result.status}`;\n}\n\n/**\n * The guard, confirm, host and re-activate sequence for an unhosted primary\n * space. A declined confirmation ends it quietly; a failure is reported to the\n * handler and thrown.\n */\nexport async function createPrimarySpace({ handler, context, guard, host, onHosted, activate }: PrimarySpaceCreationPorts): Promise<void> {\n await guard();\n if (!await handler.confirmSpaceCreation(context)) return;\n\n try {\n const created = await host();\n if (!created.success) {\n throw Object.assign(\n new Error(`Failed to create space ${context.spaceId}: ${describeFailure(created)}`),\n { cause: created },\n );\n }\n } catch (error) {\n handler.onSpaceCreationFailed?.(context, error instanceof Error ? error : new Error(String(error)));\n throw error;\n }\n onHosted();\n\n // Small delay to allow space creation to propagate\n await new Promise((resolve) => setTimeout(resolve, 100));\n\n const retry = await activate();\n if (!retry.success) {\n const err = Object.assign(\n new Error(`Failed to activate session after creating space: ${describeFailure(retry)}`),\n { cause: retry },\n );\n handler.onSpaceCreationFailed?.(context, err);\n throw err;\n }\n handler.onSpaceCreated?.(context);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/space-hosting.ts"],"sourcesContent":["/**\n * The hosting probe's read selection and classification, and the sequence that\n * creates a primary space that is not hosted. Both are loaded on first use, so\n * browser bundles carry them as a separate file rather than in their root.\n */\nimport type { ISpaceCreationHandler, SpaceCreationContext, SpaceHostResult } from \"@tinycloud/sdk-core\";\nimport type { FetchFunction, InvokeAnyEntry, ServiceHeaders } from \"@tinycloud/sdk-services\";\nimport type { SpaceHosting } from \"./authorization/NodeUserAuthorization\";\n\n/** A permission the session holds, as `TinyCloudNode` tracks it. */\nexport interface SpaceHostingOperation {\n spaceId?: string;\n resource?: string;\n service: string;\n path: string;\n action: string;\n caveats?: Record<string, unknown>[];\n}\n\n/** One read to send: `service`, `path` and `action` are what the invocation is signed for. */\nexport interface SpaceHostingRead {\n service: string;\n path: string;\n action: string;\n headers?: Record<string, string>;\n body?: string;\n}\n\nexport interface SpaceHostingPorts {\n spaceId: string;\n /** Everything the session holds, on any space: its own recap and installed runtime grants. */\n operations: readonly SpaceHostingOperation[];\n /** Whether `granted` authorizes `requested`. */\n covers(granted: SpaceHostingOperation, requested: SpaceHostingOperation): boolean;\n /** Identifies the session's services: `hosted` answers are remembered per scope, so a later session never inherits one. */\n scope: object;\n host: string;\n fetch: FetchFunction;\n /** Sign an invocation for exactly these entries. `invoke` would always sign for the session's primary space. */\n sign(entries: InvokeAnyEntry[]): ServiceHeaders;\n /** Register the session delegation with the node. Rejects on a network failure or timeout. */\n activate(signal: AbortSignal): Promise<unknown>;\n}\n\n/** Spaces the node answered `hosted` for, by session: hosting does not go away in normal operation. */\nconst hostedBy = new WeakMap<object, Set<string>>();\n\n/** Per request: the probe never holds up a caller longer. */\nconst TIMEOUT_MS = 5_000;\n\n/** A key that does not exist: a hosted space answers a read of it with a plain 404. */\nconst SPACE_PROBE_KEY = \".tinycloud-space-probe\";\nconst KV_LIST_ACTION = \"tinycloud.kv/list\";\nconst KV_METADATA_ACTION = \"tinycloud.kv/metadata\";\nconst KV_GET_ACTION = \"tinycloud.kv/get\";\nconst SPACE_INFO_ACTION = \"tinycloud.space/info\";\n\nconst isPrefix = (path: string) =>\n path === \"\" || path.endsWith(\"/\") || path.endsWith(\"/*\") || path.endsWith(\"/**\");\nconst prefixOf = (path: string) =>\n path.endsWith(\"/**\") ? path.slice(0, -2) : path.endsWith(\"/*\") ? path.slice(0, -1) : path === \"/\" ? \"\" : path;\n// A prefix grant is read at a key that does not exist; a grant on one key reads that key.\nconst keyOf = (path: string) => isPrefix(path) ? `${prefixOf(path)}${SPACE_PROBE_KEY}` : path;\n\n/**\n * The cheapest read of the space the session already holds: never one that\n * downloads a body when another exists. Reads under a caveated grant are\n * skipped, since their limits are not known here.\n */\nfunction selectRead({ spaceId, operations, covers }: SpaceHostingPorts): SpaceHostingRead | undefined {\n // Held on this space: `covers` an operation's own action and path only when it names this space and no resource.\n const held = operations.filter((operation) =>\n !operation.caveats?.length &&\n covers(operation, { spaceId, service: operation.service, path: operation.path, action: operation.action }));\n const holding = (service: string, action: string) =>\n held.filter((operation) => covers(operation, { spaceId, service, path: operation.path, action }));\n\n return [\n ...holding(\"kv\", KV_LIST_ACTION).map((operation) => ({\n service: \"kv\",\n path: isPrefix(operation.path) ? prefixOf(operation.path) : operation.path,\n action: KV_LIST_ACTION,\n headers: { \"x-tinycloud-limit\": \"1\" },\n })),\n ...holding(\"kv\", KV_METADATA_ACTION).map((operation) => ({\n service: \"kv\", path: keyOf(operation.path), action: KV_METADATA_ACTION,\n })),\n ...holding(\"space\", SPACE_INFO_ACTION).map(() => ({\n service: \"space\", path: \"\", action: SPACE_INFO_ACTION, body: \"{}\",\n })),\n ...holding(\"kv\", KV_GET_ACTION).map((operation) => ({\n service: \"kv\", path: keyOf(operation.path), action: KV_GET_ACTION,\n })),\n ].find((read) => held.some((operation) =>\n covers(operation, { spaceId, service: read.service, path: read.path, action: read.action })));\n}\n\n/** One signed read, classified. */\nasync function send(ports: SpaceHostingPorts, read: SpaceHostingRead): Promise<SpaceHosting | \"refused\"> {\n try {\n const { spaceId } = ports;\n const response = await ports.fetch(`${ports.host}/invoke`, {\n method: \"POST\",\n headers: { ...ports.sign([{ spaceId, service: read.service, path: read.path, action: read.action }]), ...read.headers },\n body: read.body,\n signal: AbortSignal.timeout(TIMEOUT_MS),\n });\n if (response.ok) {\n // A 2xx is the answer; a body that comes with it is not wanted.\n if (response.body instanceof ReadableStream) await response.body.cancel().catch(() => undefined);\n return \"hosted\";\n }\n if (response.status === 401 || response.status === 403) return \"refused\";\n if (response.status === 404) {\n // Only the node's unhosted-space 404 says \"not hosted\"; a missing key is a 404 too.\n return /space not found/i.test(await response.text()) ? \"not-hosted\" : \"hosted\";\n }\n return \"unknown\";\n } catch {\n return \"unknown\";\n }\n}\n\n/**\n * Ask the node whether the space is hosted. A 401/403 registers the session\n * delegation once and asks once more; still refused is `unknown`, as are\n * network failures, 5xx and a session that holds no read.\n */\nexport async function askSpaceHosting(ports: SpaceHostingPorts): Promise<SpaceHosting> {\n const read = selectRead(ports);\n if (!read) return \"unknown\";\n // Only a session that holds a read on the space is answered from what it already saw.\n // The address in a space id is case-insensitive; the rest of it is not.\n const key = `${ports.host} ${ports.spaceId.replace(/^(tinycloud:pkh:eip155:\\d+:)(0x[0-9a-fA-F]{40})/, (_, prefix: string, address: string) => prefix + address.toLowerCase())}`;\n if (hostedBy.get(ports.scope)?.has(key)) return \"hosted\";\n let outcome = await send(ports, read);\n if (outcome === \"refused\") {\n try {\n await ports.activate(AbortSignal.timeout(TIMEOUT_MS));\n } catch {\n return \"unknown\";\n }\n outcome = await send(ports, read);\n }\n if (outcome === \"hosted\") hostedBy.set(ports.scope, (hostedBy.get(ports.scope) ?? new Set<string>()).add(key));\n return outcome === \"refused\" ? \"unknown\" : outcome;\n}\n\ntype PrimarySpaceIdentity = Pick<SpaceCreationContext, \"address\" | \"chainId\" | \"spaceId\">;\n\nexport interface PrimarySpaceCreationPorts {\n handler: ISpaceCreationHandler;\n session?: PrimarySpaceIdentity;\n context?: SpaceCreationContext;\n currentSession?(): unknown;\n signerAddress?(): Promise<string>;\n resolveHost?(): Promise<string>;\n domain?: string;\n hostName?: string;\n /** `assertMayCreateSpace`, reporting a refusal to the handler. */\n guard(context: SpaceCreationContext): Promise<void>;\n /** Confirm that the host delegation should be submitted. */\n host(\n context: SpaceCreationContext,\n assertCurrent: () => void,\n siweParameters: (peerId: string) => {\n address: string;\n chainId: number;\n domain: string;\n issuedAt: string;\n spaceId: string;\n peerId: string;\n },\n ): Promise<SpaceHostResult>;\n /** Called once the node has the host delegation. */\n onHosted(context: SpaceCreationContext): void;\n /** Register the session delegation again. */\n activate(context: SpaceCreationContext): Promise<SpaceHostResult>;\n signal?: AbortSignal;\n}\n/** `<status> - <server text>` for a failed host call. */\nfunction describeFailure(result: SpaceHostResult): string {\n const text = result.error?.trim();\n return text ? `${result.status} - ${text}` : `${result.status}`;\n}\n\n/**\n * The guard, confirm, host and re-activate sequence for an unhosted primary\n * space. A declined confirmation ends it quietly; a failure is reported to the\n * handler and thrown.\n */\nexport async function createPrimarySpace({\n handler,\n session,\n context: existingContext,\n currentSession,\n signerAddress,\n resolveHost,\n domain,\n hostName,\n guard,\n host,\n onHosted,\n activate,\n signal,\n}: PrimarySpaceCreationPorts): Promise<void> {\n const checkCurrent = () => {\n signal?.throwIfAborted();\n if (session && currentSession?.() !== session) throw new Error(\"Space hosting session changed\");\n };\n checkCurrent();\n if (session && signerAddress && (await signerAddress()).toLowerCase() !== session.address.toLowerCase()) return;\n checkCurrent();\n const context = existingContext ?? (session && {\n address: session.address,\n chainId: session.chainId,\n spaceId: session.spaceId,\n host: await resolveHost?.() ?? hostName ?? \"\",\n });\n if (!context) throw new Error(\"Space hosting requires a session context\");\n checkCurrent();\n await guard(context);\n checkCurrent();\n const approved = await handler.confirmSpaceCreation(context);\n checkCurrent();\n if (!approved) return;\n\n try {\n const created = await host(context, checkCurrent, (peerId) => ({\n address: context.address,\n chainId: context.chainId,\n domain: domain ?? \"\",\n issuedAt: new Date().toISOString(),\n spaceId: context.spaceId,\n peerId,\n }));\n if (!created.success) {\n throw Object.assign(\n new Error(`Failed to create space ${context.spaceId}: ${describeFailure(created)}`),\n { cause: created },\n );\n }\n } catch (error) {\n handler.onSpaceCreationFailed?.(context, error instanceof Error ? error : new Error(String(error)));\n throw error;\n }\n checkCurrent();\n onHosted(context);\n\n // Small delay to allow space creation to propagate\n await new Promise((resolve) => setTimeout(resolve, 100));\n checkCurrent();\n\n const retry = await activate(context);\n checkCurrent();\n if (!retry.success) {\n const err = Object.assign(\n new Error(`Failed to activate session after creating space: ${describeFailure(retry)}`),\n { cause: retry },\n );\n handler.onSpaceCreationFailed?.(context, err);\n throw err;\n }\n handler.onSpaceCreated?.(context);\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tinycloud/node-sdk",
3
- "version": "3.12.0-beta.1",
3
+ "version": "3.12.0-beta.3",
4
4
  "description": "TinyCloud SDK for Node.js with configurable authorization strategies",
5
5
  "author": "TinyCloud, Inc.",
6
6
  "license": "EGPL",
@@ -106,9 +106,9 @@
106
106
  "dependencies": {
107
107
  "@tinycloud/share-sdk": "1.4.4",
108
108
  "@tinycloud/share-envelope": "1.5.0",
109
- "@tinycloud/replica": "0.4.4",
110
- "@tinycloud/sdk-services": "3.11.0",
111
- "@tinycloud/sdk-core": "3.11.0",
109
+ "@tinycloud/replica": "0.4.5-beta.0",
110
+ "@tinycloud/sdk-services": "3.12.0-beta.3",
111
+ "@tinycloud/sdk-core": "3.12.0-beta.3",
112
112
  "@tinycloud/node-sdk-wasm": "1.7.8",
113
113
  "siwe": "^3.0.0"
114
114
  },