@tinycloud/node-sdk 3.11.1-beta.0 → 3.12.0-beta.2

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 (35) hide show
  1. package/dist/NodeUserAuthorization-B05Wjjpb.d.cts +900 -0
  2. package/dist/NodeUserAuthorization-B05Wjjpb.d.ts +900 -0
  3. package/dist/{TinyCloudNode-AUotFGzy.d.ts → TinyCloudNode-6_IumWu0.d.cts} +32 -875
  4. package/dist/{TinyCloudNode-BmpjkpWR.d.cts → TinyCloudNode-CEFGtSWn.d.ts} +32 -875
  5. package/dist/{authority-BYSEyjDI.d.cts → authority-DODDXxoP.d.cts} +1 -1
  6. package/dist/{authority-dRnJD4iL.d.ts → authority-DXpnuId1.d.ts} +1 -1
  7. package/dist/core.cjs +182 -109
  8. package/dist/core.cjs.map +1 -1
  9. package/dist/core.d.cts +5 -4
  10. package/dist/core.d.ts +5 -4
  11. package/dist/core.js +182 -109
  12. package/dist/core.js.map +1 -1
  13. package/dist/credentials-core.d.cts +1 -1
  14. package/dist/credentials-core.d.ts +1 -1
  15. package/dist/credentials.d.cts +1 -1
  16. package/dist/credentials.d.ts +1 -1
  17. package/dist/{delegation-Ckc3UETl.d.cts → delegation-BQbAuKad.d.cts} +1 -1
  18. package/dist/{delegation-Ckc3UETl.d.ts → delegation-BQbAuKad.d.ts} +1 -1
  19. package/dist/encryption-keys.d.cts +3 -2
  20. package/dist/encryption-keys.d.ts +3 -2
  21. package/dist/index.cjs +182 -109
  22. package/dist/index.cjs.map +1 -1
  23. package/dist/index.d.cts +4 -3
  24. package/dist/index.d.ts +4 -3
  25. package/dist/index.js +182 -109
  26. package/dist/index.js.map +1 -1
  27. package/dist/replication/authority.d.cts +4 -3
  28. package/dist/replication/authority.d.ts +4 -3
  29. package/dist/space-hosting.cjs +183 -0
  30. package/dist/space-hosting.cjs.map +1 -0
  31. package/dist/space-hosting.d.cts +84 -0
  32. package/dist/space-hosting.d.ts +84 -0
  33. package/dist/space-hosting.js +157 -0
  34. package/dist/space-hosting.js.map +1 -0
  35. package/package.json +11 -1
@@ -1,8 +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-BmpjkpWR.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-BYSEyjDI.cjs';
4
+ import '../TinyCloudNode-6_IumWu0.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-DODDXxoP.cjs';
6
6
  import '@tinycloud/sdk-services/encryption/named-keys';
7
+ import '../NodeUserAuthorization-B05Wjjpb.cjs';
7
8
  import 'events';
8
- import '../delegation-Ckc3UETl.cjs';
9
+ import '../delegation-BQbAuKad.cjs';
@@ -1,8 +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-AUotFGzy.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-dRnJD4iL.js';
4
+ import '../TinyCloudNode-CEFGtSWn.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-DXpnuId1.js';
6
6
  import '@tinycloud/sdk-services/encryption/named-keys';
7
+ import '../NodeUserAuthorization-B05Wjjpb.js';
7
8
  import 'events';
8
- import '../delegation-Ckc3UETl.js';
9
+ import '../delegation-BQbAuKad.js';
@@ -0,0 +1,183 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/space-hosting.ts
21
+ var space_hosting_exports = {};
22
+ __export(space_hosting_exports, {
23
+ askSpaceHosting: () => askSpaceHosting,
24
+ createPrimarySpace: () => createPrimarySpace
25
+ });
26
+ module.exports = __toCommonJS(space_hosting_exports);
27
+ var hostedBy = /* @__PURE__ */ new WeakMap();
28
+ var TIMEOUT_MS = 5e3;
29
+ var SPACE_PROBE_KEY = ".tinycloud-space-probe";
30
+ var KV_LIST_ACTION = "tinycloud.kv/list";
31
+ var KV_METADATA_ACTION = "tinycloud.kv/metadata";
32
+ var KV_GET_ACTION = "tinycloud.kv/get";
33
+ var SPACE_INFO_ACTION = "tinycloud.space/info";
34
+ var isPrefix = (path) => path === "" || path.endsWith("/") || path.endsWith("/*") || path.endsWith("/**");
35
+ var prefixOf = (path) => path.endsWith("/**") ? path.slice(0, -2) : path.endsWith("/*") ? path.slice(0, -1) : path === "/" ? "" : path;
36
+ var keyOf = (path) => isPrefix(path) ? `${prefixOf(path)}${SPACE_PROBE_KEY}` : path;
37
+ function selectRead({ spaceId, operations, covers }) {
38
+ const held = operations.filter((operation) => !operation.caveats?.length && covers(operation, { spaceId, service: operation.service, path: operation.path, action: operation.action }));
39
+ const holding = (service, action) => held.filter((operation) => covers(operation, { spaceId, service, path: operation.path, action }));
40
+ return [
41
+ ...holding("kv", KV_LIST_ACTION).map((operation) => ({
42
+ service: "kv",
43
+ path: isPrefix(operation.path) ? prefixOf(operation.path) : operation.path,
44
+ action: KV_LIST_ACTION,
45
+ headers: { "x-tinycloud-limit": "1" }
46
+ })),
47
+ ...holding("kv", KV_METADATA_ACTION).map((operation) => ({
48
+ service: "kv",
49
+ path: keyOf(operation.path),
50
+ action: KV_METADATA_ACTION
51
+ })),
52
+ ...holding("space", SPACE_INFO_ACTION).map(() => ({
53
+ service: "space",
54
+ path: "",
55
+ action: SPACE_INFO_ACTION,
56
+ body: "{}"
57
+ })),
58
+ ...holding("kv", KV_GET_ACTION).map((operation) => ({
59
+ service: "kv",
60
+ path: keyOf(operation.path),
61
+ action: KV_GET_ACTION
62
+ }))
63
+ ].find((read) => held.some((operation) => covers(operation, { spaceId, service: read.service, path: read.path, action: read.action })));
64
+ }
65
+ async function send(ports, read) {
66
+ try {
67
+ const { spaceId } = ports;
68
+ const response = await ports.fetch(`${ports.host}/invoke`, {
69
+ method: "POST",
70
+ headers: { ...ports.sign([{ spaceId, service: read.service, path: read.path, action: read.action }]), ...read.headers },
71
+ body: read.body,
72
+ signal: AbortSignal.timeout(TIMEOUT_MS)
73
+ });
74
+ if (response.ok) {
75
+ if (response.body instanceof ReadableStream) await response.body.cancel().catch(() => void 0);
76
+ return "hosted";
77
+ }
78
+ if (response.status === 401 || response.status === 403) return "refused";
79
+ if (response.status === 404) {
80
+ return /space not found/i.test(await response.text()) ? "not-hosted" : "hosted";
81
+ }
82
+ return "unknown";
83
+ } catch {
84
+ return "unknown";
85
+ }
86
+ }
87
+ async function askSpaceHosting(ports) {
88
+ const read = selectRead(ports);
89
+ if (!read) return "unknown";
90
+ const key = `${ports.host} ${ports.spaceId.replace(/^(tinycloud:pkh:eip155:\d+:)(0x[0-9a-fA-F]{40})/, (_, prefix, address) => prefix + address.toLowerCase())}`;
91
+ if (hostedBy.get(ports.scope)?.has(key)) return "hosted";
92
+ let outcome = await send(ports, read);
93
+ if (outcome === "refused") {
94
+ try {
95
+ await ports.activate(AbortSignal.timeout(TIMEOUT_MS));
96
+ } catch {
97
+ return "unknown";
98
+ }
99
+ outcome = await send(ports, read);
100
+ }
101
+ if (outcome === "hosted") hostedBy.set(ports.scope, (hostedBy.get(ports.scope) ?? /* @__PURE__ */ new Set()).add(key));
102
+ return outcome === "refused" ? "unknown" : outcome;
103
+ }
104
+ function describeFailure(result) {
105
+ const text = result.error?.trim();
106
+ return text ? `${result.status} - ${text}` : `${result.status}`;
107
+ }
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;
143
+ try {
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
+ }));
152
+ if (!created.success) {
153
+ throw Object.assign(
154
+ new Error(`Failed to create space ${context.spaceId}: ${describeFailure(created)}`),
155
+ { cause: created }
156
+ );
157
+ }
158
+ } catch (error) {
159
+ handler.onSpaceCreationFailed?.(context, error instanceof Error ? error : new Error(String(error)));
160
+ throw error;
161
+ }
162
+ checkCurrent();
163
+ onHosted(context);
164
+ await new Promise((resolve) => setTimeout(resolve, 100));
165
+ checkCurrent();
166
+ const retry = await activate(context);
167
+ checkCurrent();
168
+ if (!retry.success) {
169
+ const err = Object.assign(
170
+ new Error(`Failed to activate session after creating space: ${describeFailure(retry)}`),
171
+ { cause: retry }
172
+ );
173
+ handler.onSpaceCreationFailed?.(context, err);
174
+ throw err;
175
+ }
176
+ handler.onSpaceCreated?.(context);
177
+ }
178
+ // Annotate the CommonJS export names for ESM import in node:
179
+ 0 && (module.exports = {
180
+ askSpaceHosting,
181
+ createPrimarySpace
182
+ });
183
+ //# sourceMappingURL=space-hosting.cjs.map
@@ -0,0 +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\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":[]}
@@ -0,0 +1,84 @@
1
+ import { ISpaceCreationHandler, SpaceCreationContext, SpaceHostResult } from '@tinycloud/sdk-core';
2
+ import { FetchFunction, InvokeAnyEntry, ServiceHeaders } from '@tinycloud/sdk-services';
3
+ import { S as SpaceHosting } from './NodeUserAuthorization-B05Wjjpb.cjs';
4
+ import 'events';
5
+
6
+ /**
7
+ * The hosting probe's read selection and classification, and the sequence that
8
+ * creates a primary space that is not hosted. Both are loaded on first use, so
9
+ * browser bundles carry them as a separate file rather than in their root.
10
+ */
11
+
12
+ /** A permission the session holds, as `TinyCloudNode` tracks it. */
13
+ interface SpaceHostingOperation {
14
+ spaceId?: string;
15
+ resource?: string;
16
+ service: string;
17
+ path: string;
18
+ action: string;
19
+ caveats?: Record<string, unknown>[];
20
+ }
21
+ /** One read to send: `service`, `path` and `action` are what the invocation is signed for. */
22
+ interface SpaceHostingRead {
23
+ service: string;
24
+ path: string;
25
+ action: string;
26
+ headers?: Record<string, string>;
27
+ body?: string;
28
+ }
29
+ interface SpaceHostingPorts {
30
+ spaceId: string;
31
+ /** Everything the session holds, on any space: its own recap and installed runtime grants. */
32
+ operations: readonly SpaceHostingOperation[];
33
+ /** Whether `granted` authorizes `requested`. */
34
+ covers(granted: SpaceHostingOperation, requested: SpaceHostingOperation): boolean;
35
+ /** Identifies the session's services: `hosted` answers are remembered per scope, so a later session never inherits one. */
36
+ scope: object;
37
+ host: string;
38
+ fetch: FetchFunction;
39
+ /** Sign an invocation for exactly these entries. `invoke` would always sign for the session's primary space. */
40
+ sign(entries: InvokeAnyEntry[]): ServiceHeaders;
41
+ /** Register the session delegation with the node. Rejects on a network failure or timeout. */
42
+ activate(signal: AbortSignal): Promise<unknown>;
43
+ }
44
+ /**
45
+ * Ask the node whether the space is hosted. A 401/403 registers the session
46
+ * delegation once and asks once more; still refused is `unknown`, as are
47
+ * network failures, 5xx and a session that holds no read.
48
+ */
49
+ declare function askSpaceHosting(ports: SpaceHostingPorts): Promise<SpaceHosting>;
50
+ type PrimarySpaceIdentity = Pick<SpaceCreationContext, "address" | "chainId" | "spaceId">;
51
+ interface PrimarySpaceCreationPorts {
52
+ handler: ISpaceCreationHandler;
53
+ session?: PrimarySpaceIdentity;
54
+ context?: SpaceCreationContext;
55
+ currentSession?(): unknown;
56
+ signerAddress?(): Promise<string>;
57
+ resolveHost?(): Promise<string>;
58
+ domain?: string;
59
+ hostName?: string;
60
+ /** `assertMayCreateSpace`, reporting a refusal to the handler. */
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>;
71
+ /** Called once the node has the host delegation. */
72
+ onHosted(context: SpaceCreationContext): void;
73
+ /** Register the session delegation again. */
74
+ activate(context: SpaceCreationContext): Promise<SpaceHostResult>;
75
+ signal?: AbortSignal;
76
+ }
77
+ /**
78
+ * The guard, confirm, host and re-activate sequence for an unhosted primary
79
+ * space. A declined confirmation ends it quietly; a failure is reported to the
80
+ * handler and thrown.
81
+ */
82
+ declare function createPrimarySpace({ handler, session, context: existingContext, currentSession, signerAddress, resolveHost, domain, hostName, guard, host, onHosted, activate, signal, }: PrimarySpaceCreationPorts): Promise<void>;
83
+
84
+ export { type PrimarySpaceCreationPorts, type SpaceHostingOperation, type SpaceHostingPorts, type SpaceHostingRead, askSpaceHosting, createPrimarySpace };
@@ -0,0 +1,84 @@
1
+ import { ISpaceCreationHandler, SpaceCreationContext, SpaceHostResult } from '@tinycloud/sdk-core';
2
+ import { FetchFunction, InvokeAnyEntry, ServiceHeaders } from '@tinycloud/sdk-services';
3
+ import { S as SpaceHosting } from './NodeUserAuthorization-B05Wjjpb.js';
4
+ import 'events';
5
+
6
+ /**
7
+ * The hosting probe's read selection and classification, and the sequence that
8
+ * creates a primary space that is not hosted. Both are loaded on first use, so
9
+ * browser bundles carry them as a separate file rather than in their root.
10
+ */
11
+
12
+ /** A permission the session holds, as `TinyCloudNode` tracks it. */
13
+ interface SpaceHostingOperation {
14
+ spaceId?: string;
15
+ resource?: string;
16
+ service: string;
17
+ path: string;
18
+ action: string;
19
+ caveats?: Record<string, unknown>[];
20
+ }
21
+ /** One read to send: `service`, `path` and `action` are what the invocation is signed for. */
22
+ interface SpaceHostingRead {
23
+ service: string;
24
+ path: string;
25
+ action: string;
26
+ headers?: Record<string, string>;
27
+ body?: string;
28
+ }
29
+ interface SpaceHostingPorts {
30
+ spaceId: string;
31
+ /** Everything the session holds, on any space: its own recap and installed runtime grants. */
32
+ operations: readonly SpaceHostingOperation[];
33
+ /** Whether `granted` authorizes `requested`. */
34
+ covers(granted: SpaceHostingOperation, requested: SpaceHostingOperation): boolean;
35
+ /** Identifies the session's services: `hosted` answers are remembered per scope, so a later session never inherits one. */
36
+ scope: object;
37
+ host: string;
38
+ fetch: FetchFunction;
39
+ /** Sign an invocation for exactly these entries. `invoke` would always sign for the session's primary space. */
40
+ sign(entries: InvokeAnyEntry[]): ServiceHeaders;
41
+ /** Register the session delegation with the node. Rejects on a network failure or timeout. */
42
+ activate(signal: AbortSignal): Promise<unknown>;
43
+ }
44
+ /**
45
+ * Ask the node whether the space is hosted. A 401/403 registers the session
46
+ * delegation once and asks once more; still refused is `unknown`, as are
47
+ * network failures, 5xx and a session that holds no read.
48
+ */
49
+ declare function askSpaceHosting(ports: SpaceHostingPorts): Promise<SpaceHosting>;
50
+ type PrimarySpaceIdentity = Pick<SpaceCreationContext, "address" | "chainId" | "spaceId">;
51
+ interface PrimarySpaceCreationPorts {
52
+ handler: ISpaceCreationHandler;
53
+ session?: PrimarySpaceIdentity;
54
+ context?: SpaceCreationContext;
55
+ currentSession?(): unknown;
56
+ signerAddress?(): Promise<string>;
57
+ resolveHost?(): Promise<string>;
58
+ domain?: string;
59
+ hostName?: string;
60
+ /** `assertMayCreateSpace`, reporting a refusal to the handler. */
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>;
71
+ /** Called once the node has the host delegation. */
72
+ onHosted(context: SpaceCreationContext): void;
73
+ /** Register the session delegation again. */
74
+ activate(context: SpaceCreationContext): Promise<SpaceHostResult>;
75
+ signal?: AbortSignal;
76
+ }
77
+ /**
78
+ * The guard, confirm, host and re-activate sequence for an unhosted primary
79
+ * space. A declined confirmation ends it quietly; a failure is reported to the
80
+ * handler and thrown.
81
+ */
82
+ declare function createPrimarySpace({ handler, session, context: existingContext, currentSession, signerAddress, resolveHost, domain, hostName, guard, host, onHosted, activate, signal, }: PrimarySpaceCreationPorts): Promise<void>;
83
+
84
+ export { type PrimarySpaceCreationPorts, type SpaceHostingOperation, type SpaceHostingPorts, type SpaceHostingRead, askSpaceHosting, createPrimarySpace };
@@ -0,0 +1,157 @@
1
+ // src/space-hosting.ts
2
+ var hostedBy = /* @__PURE__ */ new WeakMap();
3
+ var TIMEOUT_MS = 5e3;
4
+ var SPACE_PROBE_KEY = ".tinycloud-space-probe";
5
+ var KV_LIST_ACTION = "tinycloud.kv/list";
6
+ var KV_METADATA_ACTION = "tinycloud.kv/metadata";
7
+ var KV_GET_ACTION = "tinycloud.kv/get";
8
+ var SPACE_INFO_ACTION = "tinycloud.space/info";
9
+ var isPrefix = (path) => path === "" || path.endsWith("/") || path.endsWith("/*") || path.endsWith("/**");
10
+ var prefixOf = (path) => path.endsWith("/**") ? path.slice(0, -2) : path.endsWith("/*") ? path.slice(0, -1) : path === "/" ? "" : path;
11
+ var keyOf = (path) => isPrefix(path) ? `${prefixOf(path)}${SPACE_PROBE_KEY}` : path;
12
+ function selectRead({ spaceId, operations, covers }) {
13
+ const held = operations.filter((operation) => !operation.caveats?.length && covers(operation, { spaceId, service: operation.service, path: operation.path, action: operation.action }));
14
+ const holding = (service, action) => held.filter((operation) => covers(operation, { spaceId, service, path: operation.path, action }));
15
+ return [
16
+ ...holding("kv", KV_LIST_ACTION).map((operation) => ({
17
+ service: "kv",
18
+ path: isPrefix(operation.path) ? prefixOf(operation.path) : operation.path,
19
+ action: KV_LIST_ACTION,
20
+ headers: { "x-tinycloud-limit": "1" }
21
+ })),
22
+ ...holding("kv", KV_METADATA_ACTION).map((operation) => ({
23
+ service: "kv",
24
+ path: keyOf(operation.path),
25
+ action: KV_METADATA_ACTION
26
+ })),
27
+ ...holding("space", SPACE_INFO_ACTION).map(() => ({
28
+ service: "space",
29
+ path: "",
30
+ action: SPACE_INFO_ACTION,
31
+ body: "{}"
32
+ })),
33
+ ...holding("kv", KV_GET_ACTION).map((operation) => ({
34
+ service: "kv",
35
+ path: keyOf(operation.path),
36
+ action: KV_GET_ACTION
37
+ }))
38
+ ].find((read) => held.some((operation) => covers(operation, { spaceId, service: read.service, path: read.path, action: read.action })));
39
+ }
40
+ async function send(ports, read) {
41
+ try {
42
+ const { spaceId } = ports;
43
+ const response = await ports.fetch(`${ports.host}/invoke`, {
44
+ method: "POST",
45
+ headers: { ...ports.sign([{ spaceId, service: read.service, path: read.path, action: read.action }]), ...read.headers },
46
+ body: read.body,
47
+ signal: AbortSignal.timeout(TIMEOUT_MS)
48
+ });
49
+ if (response.ok) {
50
+ if (response.body instanceof ReadableStream) await response.body.cancel().catch(() => void 0);
51
+ return "hosted";
52
+ }
53
+ if (response.status === 401 || response.status === 403) return "refused";
54
+ if (response.status === 404) {
55
+ return /space not found/i.test(await response.text()) ? "not-hosted" : "hosted";
56
+ }
57
+ return "unknown";
58
+ } catch {
59
+ return "unknown";
60
+ }
61
+ }
62
+ async function askSpaceHosting(ports) {
63
+ const read = selectRead(ports);
64
+ if (!read) return "unknown";
65
+ const key = `${ports.host} ${ports.spaceId.replace(/^(tinycloud:pkh:eip155:\d+:)(0x[0-9a-fA-F]{40})/, (_, prefix, address) => prefix + address.toLowerCase())}`;
66
+ if (hostedBy.get(ports.scope)?.has(key)) return "hosted";
67
+ let outcome = await send(ports, read);
68
+ if (outcome === "refused") {
69
+ try {
70
+ await ports.activate(AbortSignal.timeout(TIMEOUT_MS));
71
+ } catch {
72
+ return "unknown";
73
+ }
74
+ outcome = await send(ports, read);
75
+ }
76
+ if (outcome === "hosted") hostedBy.set(ports.scope, (hostedBy.get(ports.scope) ?? /* @__PURE__ */ new Set()).add(key));
77
+ return outcome === "refused" ? "unknown" : outcome;
78
+ }
79
+ function describeFailure(result) {
80
+ const text = result.error?.trim();
81
+ return text ? `${result.status} - ${text}` : `${result.status}`;
82
+ }
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;
118
+ try {
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
+ }));
127
+ if (!created.success) {
128
+ throw Object.assign(
129
+ new Error(`Failed to create space ${context.spaceId}: ${describeFailure(created)}`),
130
+ { cause: created }
131
+ );
132
+ }
133
+ } catch (error) {
134
+ handler.onSpaceCreationFailed?.(context, error instanceof Error ? error : new Error(String(error)));
135
+ throw error;
136
+ }
137
+ checkCurrent();
138
+ onHosted(context);
139
+ await new Promise((resolve) => setTimeout(resolve, 100));
140
+ checkCurrent();
141
+ const retry = await activate(context);
142
+ checkCurrent();
143
+ if (!retry.success) {
144
+ const err = Object.assign(
145
+ new Error(`Failed to activate session after creating space: ${describeFailure(retry)}`),
146
+ { cause: retry }
147
+ );
148
+ handler.onSpaceCreationFailed?.(context, err);
149
+ throw err;
150
+ }
151
+ handler.onSpaceCreated?.(context);
152
+ }
153
+ export {
154
+ askSpaceHosting,
155
+ createPrimarySpace
156
+ };
157
+ //# sourceMappingURL=space-hosting.js.map
@@ -0,0 +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\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.11.1-beta.0",
3
+ "version": "3.12.0-beta.2",
4
4
  "description": "TinyCloud SDK for Node.js with configurable authorization strategies",
5
5
  "author": "TinyCloud, Inc.",
6
6
  "license": "EGPL",
@@ -29,6 +29,16 @@
29
29
  "default": "./dist/encryption-keys.cjs"
30
30
  }
31
31
  },
32
+ "./space-hosting": {
33
+ "import": {
34
+ "types": "./dist/space-hosting.d.ts",
35
+ "default": "./dist/space-hosting.js"
36
+ },
37
+ "require": {
38
+ "types": "./dist/space-hosting.d.cts",
39
+ "default": "./dist/space-hosting.cjs"
40
+ }
41
+ },
32
42
  "./credential-pending-store": {
33
43
  "types": "./dist/credential-pending-store.d.ts",
34
44
  "import": "./dist/credential-pending-store.js",