@tinycloudlabs/sdk-core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/LICENSE.md +320 -0
  2. package/dist/TinyCloud.d.ts +206 -0
  3. package/dist/TinyCloud.d.ts.map +1 -0
  4. package/dist/TinyCloud.js +244 -0
  5. package/dist/TinyCloud.js.map +1 -0
  6. package/dist/TinyCloud.schema.d.ts +173 -0
  7. package/dist/TinyCloud.schema.d.ts.map +1 -0
  8. package/dist/TinyCloud.schema.js +136 -0
  9. package/dist/TinyCloud.schema.js.map +1 -0
  10. package/dist/TinyCloud.schema.test.d.ts +5 -0
  11. package/dist/TinyCloud.schema.test.d.ts.map +1 -0
  12. package/dist/TinyCloud.schema.test.js +286 -0
  13. package/dist/TinyCloud.schema.test.js.map +1 -0
  14. package/dist/authorization/CapabilityKeyRegistry.d.ts +317 -0
  15. package/dist/authorization/CapabilityKeyRegistry.d.ts.map +1 -0
  16. package/dist/authorization/CapabilityKeyRegistry.js +509 -0
  17. package/dist/authorization/CapabilityKeyRegistry.js.map +1 -0
  18. package/dist/authorization/authorization.schema.d.ts +233 -0
  19. package/dist/authorization/authorization.schema.d.ts.map +1 -0
  20. package/dist/authorization/authorization.schema.js +220 -0
  21. package/dist/authorization/authorization.schema.js.map +1 -0
  22. package/dist/authorization/authorization.schema.test.d.ts +5 -0
  23. package/dist/authorization/authorization.schema.test.d.ts.map +1 -0
  24. package/dist/authorization/authorization.schema.test.js +618 -0
  25. package/dist/authorization/authorization.schema.test.js.map +1 -0
  26. package/dist/authorization/index.d.ts +38 -0
  27. package/dist/authorization/index.d.ts.map +1 -0
  28. package/dist/authorization/index.js +52 -0
  29. package/dist/authorization/index.js.map +1 -0
  30. package/dist/authorization/spaceCreation.d.ts +96 -0
  31. package/dist/authorization/spaceCreation.d.ts.map +1 -0
  32. package/dist/authorization/spaceCreation.js +35 -0
  33. package/dist/authorization/spaceCreation.js.map +1 -0
  34. package/dist/authorization/spaceCreation.schema.d.ts +67 -0
  35. package/dist/authorization/spaceCreation.schema.d.ts.map +1 -0
  36. package/dist/authorization/spaceCreation.schema.js +95 -0
  37. package/dist/authorization/spaceCreation.schema.js.map +1 -0
  38. package/dist/authorization/spaceCreation.schema.test.d.ts +5 -0
  39. package/dist/authorization/spaceCreation.schema.test.d.ts.map +1 -0
  40. package/dist/authorization/spaceCreation.schema.test.js +168 -0
  41. package/dist/authorization/spaceCreation.schema.test.js.map +1 -0
  42. package/dist/authorization/strategies.d.ts +134 -0
  43. package/dist/authorization/strategies.d.ts.map +1 -0
  44. package/dist/authorization/strategies.js +15 -0
  45. package/dist/authorization/strategies.js.map +1 -0
  46. package/dist/authorization/strategies.schema.d.ts +185 -0
  47. package/dist/authorization/strategies.schema.d.ts.map +1 -0
  48. package/dist/authorization/strategies.schema.js +147 -0
  49. package/dist/authorization/strategies.schema.js.map +1 -0
  50. package/dist/authorization/strategies.schema.test.d.ts +5 -0
  51. package/dist/authorization/strategies.schema.test.d.ts.map +1 -0
  52. package/dist/authorization/strategies.schema.test.js +253 -0
  53. package/dist/authorization/strategies.schema.test.js.map +1 -0
  54. package/dist/delegations/DelegationManager.d.ts +164 -0
  55. package/dist/delegations/DelegationManager.d.ts.map +1 -0
  56. package/dist/delegations/DelegationManager.js +428 -0
  57. package/dist/delegations/DelegationManager.js.map +1 -0
  58. package/dist/delegations/SharingService.d.ts +279 -0
  59. package/dist/delegations/SharingService.d.ts.map +1 -0
  60. package/dist/delegations/SharingService.js +558 -0
  61. package/dist/delegations/SharingService.js.map +1 -0
  62. package/dist/delegations/SharingService.schema.d.ts +401 -0
  63. package/dist/delegations/SharingService.schema.d.ts.map +1 -0
  64. package/dist/delegations/SharingService.schema.js +211 -0
  65. package/dist/delegations/SharingService.schema.js.map +1 -0
  66. package/dist/delegations/index.d.ts +38 -0
  67. package/dist/delegations/index.d.ts.map +1 -0
  68. package/dist/delegations/index.js +42 -0
  69. package/dist/delegations/index.js.map +1 -0
  70. package/dist/delegations/types.d.ts +13 -0
  71. package/dist/delegations/types.d.ts.map +1 -0
  72. package/dist/delegations/types.js +42 -0
  73. package/dist/delegations/types.js.map +1 -0
  74. package/dist/delegations/types.schema.d.ts +1641 -0
  75. package/dist/delegations/types.schema.d.ts.map +1 -0
  76. package/dist/delegations/types.schema.js +535 -0
  77. package/dist/delegations/types.schema.js.map +1 -0
  78. package/dist/delegations/types.schema.test.d.ts +5 -0
  79. package/dist/delegations/types.schema.test.d.ts.map +1 -0
  80. package/dist/delegations/types.schema.test.js +627 -0
  81. package/dist/delegations/types.schema.test.js.map +1 -0
  82. package/dist/index.d.ts +22 -0
  83. package/dist/index.d.ts.map +1 -0
  84. package/dist/index.js +52 -0
  85. package/dist/index.js.map +1 -0
  86. package/dist/json-schema.d.ts +327 -0
  87. package/dist/json-schema.d.ts.map +1 -0
  88. package/dist/json-schema.js +703 -0
  89. package/dist/json-schema.js.map +1 -0
  90. package/dist/json-schema.test.d.ts +7 -0
  91. package/dist/json-schema.test.d.ts.map +1 -0
  92. package/dist/json-schema.test.js +365 -0
  93. package/dist/json-schema.test.js.map +1 -0
  94. package/dist/signer.d.ts +28 -0
  95. package/dist/signer.d.ts.map +1 -0
  96. package/dist/signer.js +2 -0
  97. package/dist/signer.js.map +1 -0
  98. package/dist/space.d.ts +53 -0
  99. package/dist/space.d.ts.map +1 -0
  100. package/dist/space.js +67 -0
  101. package/dist/space.js.map +1 -0
  102. package/dist/space.schema.d.ts +65 -0
  103. package/dist/space.schema.d.ts.map +1 -0
  104. package/dist/space.schema.js +65 -0
  105. package/dist/space.schema.js.map +1 -0
  106. package/dist/space.schema.test.d.ts +5 -0
  107. package/dist/space.schema.test.d.ts.map +1 -0
  108. package/dist/space.schema.test.js +148 -0
  109. package/dist/space.schema.test.js.map +1 -0
  110. package/dist/spaces/Space.d.ts +175 -0
  111. package/dist/spaces/Space.d.ts.map +1 -0
  112. package/dist/spaces/Space.js +84 -0
  113. package/dist/spaces/Space.js.map +1 -0
  114. package/dist/spaces/SpaceService.d.ts +271 -0
  115. package/dist/spaces/SpaceService.d.ts.map +1 -0
  116. package/dist/spaces/SpaceService.js +715 -0
  117. package/dist/spaces/SpaceService.js.map +1 -0
  118. package/dist/spaces/index.d.ts +11 -0
  119. package/dist/spaces/index.d.ts.map +1 -0
  120. package/dist/spaces/index.js +20 -0
  121. package/dist/spaces/index.js.map +1 -0
  122. package/dist/spaces/spaces.schema.d.ts +421 -0
  123. package/dist/spaces/spaces.schema.d.ts.map +1 -0
  124. package/dist/spaces/spaces.schema.js +342 -0
  125. package/dist/spaces/spaces.schema.js.map +1 -0
  126. package/dist/spaces/spaces.schema.test.d.ts +5 -0
  127. package/dist/spaces/spaces.schema.test.d.ts.map +1 -0
  128. package/dist/spaces/spaces.schema.test.js +471 -0
  129. package/dist/spaces/spaces.schema.test.js.map +1 -0
  130. package/dist/storage.d.ts +47 -0
  131. package/dist/storage.d.ts.map +1 -0
  132. package/dist/storage.js +14 -0
  133. package/dist/storage.js.map +1 -0
  134. package/dist/storage.schema.d.ts +277 -0
  135. package/dist/storage.schema.d.ts.map +1 -0
  136. package/dist/storage.schema.js +185 -0
  137. package/dist/storage.schema.js.map +1 -0
  138. package/dist/storage.schema.test.d.ts +5 -0
  139. package/dist/storage.schema.test.d.ts.map +1 -0
  140. package/dist/storage.schema.test.js +346 -0
  141. package/dist/storage.schema.test.js.map +1 -0
  142. package/dist/userAuthorization.d.ts +99 -0
  143. package/dist/userAuthorization.d.ts.map +1 -0
  144. package/dist/userAuthorization.js +3 -0
  145. package/dist/userAuthorization.js.map +1 -0
  146. package/dist/userAuthorization.schema.d.ts +259 -0
  147. package/dist/userAuthorization.schema.d.ts.map +1 -0
  148. package/dist/userAuthorization.schema.js +175 -0
  149. package/dist/userAuthorization.schema.js.map +1 -0
  150. package/dist/userAuthorization.schema.test.d.ts +5 -0
  151. package/dist/userAuthorization.schema.test.d.ts.map +1 -0
  152. package/dist/userAuthorization.schema.test.js +356 -0
  153. package/dist/userAuthorization.schema.test.js.map +1 -0
  154. package/dist/version.d.ts +30 -0
  155. package/dist/version.d.ts.map +1 -0
  156. package/dist/version.js +54 -0
  157. package/dist/version.js.map +1 -0
  158. package/dist/wasm-validation.d.ts +287 -0
  159. package/dist/wasm-validation.d.ts.map +1 -0
  160. package/dist/wasm-validation.js +219 -0
  161. package/dist/wasm-validation.js.map +1 -0
  162. package/dist/wasm-validation.test.d.ts +5 -0
  163. package/dist/wasm-validation.test.d.ts.map +1 -0
  164. package/dist/wasm-validation.test.js +233 -0
  165. package/dist/wasm-validation.test.js.map +1 -0
  166. package/package.json +40 -0
@@ -0,0 +1,715 @@
1
+ /**
2
+ * SpaceService - Global singleton for managing spaces (owned and delegated).
3
+ *
4
+ * SpaceService provides a unified interface for discovering, creating,
5
+ * and accessing spaces. It handles both owned spaces (created by the user)
6
+ * and delegated spaces (shared by other users).
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+ import { ok, err, serviceError } from "@tinycloudlabs/sdk-services";
11
+ import { Space, } from "./Space";
12
+ import { validateServerDelegationsResponse, validateServerOwnedSpacesResponse, validateServerCreateSpaceResponse, validateServerSpaceInfoResponse, } from "./spaces.schema.js";
13
+ // =============================================================================
14
+ // Service Name and Error Codes
15
+ // =============================================================================
16
+ const SERVICE_NAME = "space";
17
+ /**
18
+ * Error codes for SpaceService operations.
19
+ */
20
+ export const SpaceErrorCodes = {
21
+ /** Space not found */
22
+ NOT_FOUND: "SPACE_NOT_FOUND",
23
+ /** Space already exists */
24
+ ALREADY_EXISTS: "SPACE_ALREADY_EXISTS",
25
+ /** Creation failed */
26
+ CREATION_FAILED: "SPACE_CREATION_FAILED",
27
+ /** Authentication required */
28
+ AUTH_REQUIRED: "AUTH_REQUIRED",
29
+ /** Invalid space name or URI */
30
+ INVALID_NAME: "INVALID_SPACE_NAME",
31
+ /** Network error */
32
+ NETWORK_ERROR: "NETWORK_ERROR",
33
+ /** Not initialized */
34
+ NOT_INITIALIZED: "NOT_INITIALIZED",
35
+ };
36
+ // =============================================================================
37
+ // Space URI Utilities
38
+ // =============================================================================
39
+ /**
40
+ * Parse a space URI to extract components.
41
+ *
42
+ * Full URI format: `tinycloud:pkh:eip155:{chainId}:{address}:{name}`
43
+ * Short name format: `{name}`
44
+ *
45
+ * @param uri - The space URI or short name
46
+ * @returns Parsed components or null if invalid
47
+ */
48
+ export function parseSpaceUri(uri) {
49
+ // Full URI format: tinycloud:pkh:eip155:{chainId}:{address}:{name}
50
+ const fullUriMatch = uri.match(/^tinycloud:pkh:eip155:(\d+):(0x[a-fA-F0-9]{40}):(.+)$/);
51
+ if (fullUriMatch) {
52
+ const [, chainId, address, name] = fullUriMatch;
53
+ return {
54
+ owner: `did:pkh:eip155:${chainId}:${address}`,
55
+ name,
56
+ chainId,
57
+ address,
58
+ };
59
+ }
60
+ // Short name format - just return the name, owner will be inferred
61
+ if (/^[a-zA-Z0-9_-]+$/.test(uri)) {
62
+ return {
63
+ owner: "", // Will be filled in from session
64
+ name: uri,
65
+ };
66
+ }
67
+ return null;
68
+ }
69
+ /**
70
+ * Build a full space URI from components.
71
+ *
72
+ * @param owner - Owner DID (did:pkh:eip155:{chainId}:{address})
73
+ * @param name - Space name
74
+ * @returns Full space URI
75
+ */
76
+ export function buildSpaceUri(owner, name) {
77
+ // Extract chain ID and address from PKH DID
78
+ const pkhMatch = owner.match(/^did:pkh:eip155:(\d+):(0x[a-fA-F0-9]{40})$/);
79
+ if (pkhMatch) {
80
+ const [, chainId, address] = pkhMatch;
81
+ return `tinycloud:pkh:eip155:${chainId}:${address}:${name}`;
82
+ }
83
+ // Fallback - shouldn't happen with valid PKH DIDs
84
+ return `tinycloud:${owner}:${name}`;
85
+ }
86
+ // =============================================================================
87
+ // Server Response Transformation
88
+ // =============================================================================
89
+ /**
90
+ * Transform validated server delegation response to SDK's Delegation[] format.
91
+ *
92
+ * Server returns { [cid: string]: DelegationInfo } where:
93
+ * - Key is the delegation CID
94
+ * - Value is DelegationInfo with delegator, delegate, capabilities, etc.
95
+ *
96
+ * SDK expects Delegation[] with cid field included.
97
+ *
98
+ * @param validatedData - Pre-validated server response from validateServerDelegationsResponse()
99
+ * @param defaultSpaceId - Default space ID to use if not extractable from resource
100
+ */
101
+ function transformServerDelegations(validatedData, defaultSpaceId) {
102
+ const result = [];
103
+ for (const [cid, info] of Object.entries(validatedData)) {
104
+ // Extract path from capabilities (use first capability's resource path)
105
+ const capabilities = info.capabilities;
106
+ let path = "";
107
+ let spaceId = defaultSpaceId;
108
+ const actions = [];
109
+ for (const cap of capabilities) {
110
+ actions.push(cap.ability);
111
+ // Parse resource to extract space and path
112
+ // Resource format: tinycloud:pkh:eip155:{chainId}:{address}:{spaceName}/{service}/{path}
113
+ const resourceMatch = cap.resource.match(/^(tinycloud:pkh:eip155:\d+:0x[a-fA-F0-9]+:[^/]+)\/[^/]+\/(.*)$/);
114
+ if (resourceMatch) {
115
+ spaceId = resourceMatch[1];
116
+ path = resourceMatch[2] || "";
117
+ }
118
+ }
119
+ // Extract first string parent CID (server may return byte arrays for some CIDs)
120
+ const firstStringParent = info.parents?.find((p) => typeof p === 'string');
121
+ result.push({
122
+ cid,
123
+ delegateDID: info.delegate,
124
+ delegatorDID: info.delegator,
125
+ spaceId,
126
+ path,
127
+ actions,
128
+ expiry: info.expiry ? new Date(info.expiry) : new Date(Date.now() + 24 * 60 * 60 * 1000),
129
+ isRevoked: false,
130
+ createdAt: info.issued_at ? new Date(info.issued_at) : undefined,
131
+ parentCid: firstStringParent,
132
+ });
133
+ }
134
+ return result;
135
+ }
136
+ // =============================================================================
137
+ // Implementation
138
+ // =============================================================================
139
+ /**
140
+ * SpaceService - Global singleton for managing spaces.
141
+ *
142
+ * @example
143
+ * ```typescript
144
+ * const spaceService = new SpaceService({
145
+ * hosts: ['https://node.tinycloud.xyz'],
146
+ * session,
147
+ * invoke,
148
+ * });
149
+ *
150
+ * // List all accessible spaces
151
+ * const result = await spaceService.list();
152
+ * if (result.ok) {
153
+ * for (const space of result.data) {
154
+ * console.log(`${space.name} (${space.type})`);
155
+ * }
156
+ * }
157
+ *
158
+ * // Create a new space
159
+ * const createResult = await spaceService.create('photos');
160
+ *
161
+ * // Get a space object for operations
162
+ * const space = spaceService.get('photos');
163
+ * await space.kv.put('album/vacation', { photos: [...] });
164
+ * ```
165
+ */
166
+ export class SpaceService {
167
+ /**
168
+ * Create a new SpaceService instance.
169
+ *
170
+ * @param config - Service configuration
171
+ */
172
+ constructor(config) {
173
+ /** Cache of created Space objects */
174
+ this.spaceCache = new Map();
175
+ /** Cache of space info */
176
+ this.infoCache = new Map();
177
+ /** Cache TTL in milliseconds (5 minutes) */
178
+ this.cacheTTL = 5 * 60 * 1000;
179
+ this.hosts = config.hosts;
180
+ this.session = config.session;
181
+ this.invoke = config.invoke;
182
+ this.fetchFn = config.fetch ?? globalThis.fetch.bind(globalThis);
183
+ this.capabilityRegistry = config.capabilityRegistry;
184
+ this.createKVServiceFn = config.createKVService;
185
+ this._userDid = config.userDid;
186
+ this.sharingService = config.sharingService;
187
+ this.createDelegationFn = config.createDelegation;
188
+ }
189
+ /**
190
+ * Update the service configuration.
191
+ */
192
+ updateConfig(config) {
193
+ if (config.hosts)
194
+ this.hosts = config.hosts;
195
+ if (config.session)
196
+ this.session = config.session;
197
+ if (config.invoke)
198
+ this.invoke = config.invoke;
199
+ if (config.fetch)
200
+ this.fetchFn = config.fetch;
201
+ if (config.capabilityRegistry)
202
+ this.capabilityRegistry = config.capabilityRegistry;
203
+ if (config.createKVService)
204
+ this.createKVServiceFn = config.createKVService;
205
+ if (config.userDid !== undefined)
206
+ this._userDid = config.userDid;
207
+ if (config.sharingService)
208
+ this.sharingService = config.sharingService;
209
+ if (config.createDelegation)
210
+ this.createDelegationFn = config.createDelegation;
211
+ // Clear caches when config changes
212
+ this.spaceCache.clear();
213
+ this.infoCache.clear();
214
+ }
215
+ /**
216
+ * Get the current user's primary space ID.
217
+ */
218
+ getCurrentSpaceId() {
219
+ return this.session?.spaceId;
220
+ }
221
+ /**
222
+ * Get the primary host URL.
223
+ */
224
+ get host() {
225
+ return this.hosts[0];
226
+ }
227
+ /**
228
+ * Get the current user's PKH DID.
229
+ */
230
+ get userDid() {
231
+ // Return explicitly set user DID, or try to derive from verificationMethod
232
+ if (this._userDid) {
233
+ return this._userDid;
234
+ }
235
+ // verificationMethod might be did:key format - cannot derive PKH from it
236
+ // The caller should provide userDid explicitly for full functionality
237
+ return undefined;
238
+ }
239
+ // ===========================================================================
240
+ // List Spaces
241
+ // ===========================================================================
242
+ /**
243
+ * List all spaces the user has access to.
244
+ *
245
+ * Combines owned spaces (from the server) with delegated spaces
246
+ * (from the capability registry).
247
+ */
248
+ async list() {
249
+ if (!this.session) {
250
+ return err(serviceError(SpaceErrorCodes.AUTH_REQUIRED, "Authentication required", SERVICE_NAME));
251
+ }
252
+ try {
253
+ const spaces = [];
254
+ // 1. Get owned spaces from the server
255
+ const ownedResult = await this.listOwnedSpaces();
256
+ if (ownedResult.ok) {
257
+ spaces.push(...ownedResult.data);
258
+ }
259
+ // 2. Get delegated spaces from capability registry
260
+ if (this.capabilityRegistry) {
261
+ const delegatedSpaces = this.discoverDelegatedSpaces();
262
+ spaces.push(...delegatedSpaces);
263
+ }
264
+ // Remove duplicates (prefer owned over delegated)
265
+ const uniqueSpaces = this.deduplicateSpaces(spaces);
266
+ return ok(uniqueSpaces);
267
+ }
268
+ catch (error) {
269
+ return err(serviceError(SpaceErrorCodes.NETWORK_ERROR, `Failed to list spaces: ${String(error)}`, SERVICE_NAME, { cause: error instanceof Error ? error : undefined }));
270
+ }
271
+ }
272
+ /**
273
+ * List owned spaces from the server.
274
+ */
275
+ async listOwnedSpaces() {
276
+ try {
277
+ const headers = this.invoke(this.session, "space", "", "tinycloud.space/list");
278
+ const response = await this.fetchFn(`${this.host}/invoke`, {
279
+ method: "POST",
280
+ headers,
281
+ });
282
+ if (!response.ok) {
283
+ const errorText = await response.text();
284
+ return err(serviceError(SpaceErrorCodes.NETWORK_ERROR, `Failed to list owned spaces: ${response.status} - ${errorText}`, SERVICE_NAME, { meta: { status: response.status } }));
285
+ }
286
+ const rawData = await response.json();
287
+ // Validate server response
288
+ const validationResult = validateServerOwnedSpacesResponse(rawData);
289
+ if (!validationResult.ok) {
290
+ return err(serviceError(SpaceErrorCodes.NETWORK_ERROR, validationResult.error.message, SERVICE_NAME, { meta: validationResult.error.meta }));
291
+ }
292
+ const spaces = validationResult.data.map((item) => ({
293
+ id: item.id,
294
+ name: item.name ?? this.extractNameFromId(item.id),
295
+ owner: item.owner,
296
+ type: "owned",
297
+ permissions: ["*"], // Full permissions for owned spaces
298
+ }));
299
+ return ok(spaces);
300
+ }
301
+ catch (error) {
302
+ return err(serviceError(SpaceErrorCodes.NETWORK_ERROR, `Network error listing owned spaces: ${String(error)}`, SERVICE_NAME, { cause: error instanceof Error ? error : undefined }));
303
+ }
304
+ }
305
+ /**
306
+ * Discover delegated spaces from the capability registry.
307
+ */
308
+ discoverDelegatedSpaces() {
309
+ if (!this.capabilityRegistry) {
310
+ return [];
311
+ }
312
+ const spaces = new Map();
313
+ // Get all capabilities and extract unique space IDs
314
+ const capabilities = this.capabilityRegistry.getAllCapabilities();
315
+ for (const capability of capabilities) {
316
+ const spaceId = capability.delegation.spaceId;
317
+ // Skip if we already have this space
318
+ if (spaces.has(spaceId)) {
319
+ const existing = spaces.get(spaceId);
320
+ // Merge permissions
321
+ if (existing.permissions) {
322
+ const actions = capability.delegation.actions;
323
+ for (const action of actions) {
324
+ if (!existing.permissions.includes(action)) {
325
+ existing.permissions.push(action);
326
+ }
327
+ }
328
+ }
329
+ continue;
330
+ }
331
+ // Skip if this is the user's own space
332
+ if (spaceId === this.session?.spaceId) {
333
+ continue;
334
+ }
335
+ const parsed = parseSpaceUri(spaceId);
336
+ spaces.set(spaceId, {
337
+ id: spaceId,
338
+ name: parsed?.name ?? this.extractNameFromId(spaceId),
339
+ owner: capability.delegation.delegatorDID ?? parsed?.owner ?? "",
340
+ type: "delegated",
341
+ permissions: [...capability.delegation.actions],
342
+ expiresAt: capability.expiresAt,
343
+ });
344
+ }
345
+ return Array.from(spaces.values());
346
+ }
347
+ /**
348
+ * Extract space name from a full space ID.
349
+ */
350
+ extractNameFromId(id) {
351
+ const parsed = parseSpaceUri(id);
352
+ if (parsed) {
353
+ return parsed.name;
354
+ }
355
+ // Fallback: take last segment
356
+ const parts = id.split(":");
357
+ return parts[parts.length - 1] || id;
358
+ }
359
+ /**
360
+ * Deduplicate spaces, preferring owned over delegated.
361
+ */
362
+ deduplicateSpaces(spaces) {
363
+ const seen = new Map();
364
+ for (const space of spaces) {
365
+ const existing = seen.get(space.id);
366
+ if (!existing || (existing.type === "delegated" && space.type === "owned")) {
367
+ seen.set(space.id, space);
368
+ }
369
+ }
370
+ return Array.from(seen.values());
371
+ }
372
+ // ===========================================================================
373
+ // Create Space
374
+ // ===========================================================================
375
+ /**
376
+ * Create a new space.
377
+ *
378
+ * @param name - The name for the new space
379
+ */
380
+ async create(name) {
381
+ if (!this.session) {
382
+ return err(serviceError(SpaceErrorCodes.AUTH_REQUIRED, "Authentication required", SERVICE_NAME));
383
+ }
384
+ // Validate name
385
+ if (!name || !/^[a-zA-Z0-9_-]+$/.test(name)) {
386
+ return err(serviceError(SpaceErrorCodes.INVALID_NAME, "Space name must contain only alphanumeric characters, underscores, and hyphens", SERVICE_NAME));
387
+ }
388
+ try {
389
+ const headers = this.invoke(this.session, "space", name, "tinycloud.space/create");
390
+ const response = await this.fetchFn(`${this.host}/invoke`, {
391
+ method: "POST",
392
+ headers,
393
+ body: JSON.stringify({ name }),
394
+ });
395
+ if (!response.ok) {
396
+ const errorText = await response.text();
397
+ if (response.status === 409) {
398
+ return err(serviceError(SpaceErrorCodes.ALREADY_EXISTS, `Space "${name}" already exists`, SERVICE_NAME));
399
+ }
400
+ return err(serviceError(SpaceErrorCodes.CREATION_FAILED, `Failed to create space: ${response.status} - ${errorText}`, SERVICE_NAME, { meta: { status: response.status } }));
401
+ }
402
+ const rawData = await response.json();
403
+ // Validate server response
404
+ const validationResult = validateServerCreateSpaceResponse(rawData);
405
+ if (!validationResult.ok) {
406
+ return err(serviceError(SpaceErrorCodes.CREATION_FAILED, validationResult.error.message, SERVICE_NAME, { meta: validationResult.error.meta }));
407
+ }
408
+ const spaceInfo = {
409
+ id: validationResult.data.id,
410
+ name: validationResult.data.name || name,
411
+ owner: validationResult.data.owner || this.userDid || "",
412
+ type: "owned",
413
+ permissions: ["*"],
414
+ };
415
+ // Cache the info
416
+ this.infoCache.set(spaceInfo.id, { info: spaceInfo, cachedAt: Date.now() });
417
+ return ok(spaceInfo);
418
+ }
419
+ catch (error) {
420
+ return err(serviceError(SpaceErrorCodes.NETWORK_ERROR, `Network error creating space: ${String(error)}`, SERVICE_NAME, { cause: error instanceof Error ? error : undefined }));
421
+ }
422
+ }
423
+ // ===========================================================================
424
+ // Get Space
425
+ // ===========================================================================
426
+ /**
427
+ * Get a Space object by name or full URI.
428
+ *
429
+ * @param nameOrUri - Short name or full URI
430
+ */
431
+ get(nameOrUri) {
432
+ // Resolve the full space ID
433
+ const spaceId = this.resolveSpaceId(nameOrUri);
434
+ // Check cache
435
+ const cached = this.spaceCache.get(spaceId);
436
+ if (cached) {
437
+ return cached;
438
+ }
439
+ // Create new Space object
440
+ const parsed = parseSpaceUri(spaceId);
441
+ const name = parsed?.name ?? this.extractNameFromId(spaceId);
442
+ const config = {
443
+ id: spaceId,
444
+ name,
445
+ createKV: this.createSpaceScopedKV.bind(this),
446
+ createDelegations: this.createSpaceScopedDelegations.bind(this),
447
+ createSharing: this.createSpaceScopedSharing.bind(this),
448
+ getInfo: this.getSpaceInfo.bind(this),
449
+ };
450
+ const space = new Space(config);
451
+ this.spaceCache.set(spaceId, space);
452
+ return space;
453
+ }
454
+ /**
455
+ * Resolve a name or URI to a full space ID.
456
+ */
457
+ resolveSpaceId(nameOrUri) {
458
+ const parsed = parseSpaceUri(nameOrUri);
459
+ if (!parsed) {
460
+ // Invalid format, return as-is
461
+ return nameOrUri;
462
+ }
463
+ if (parsed.owner) {
464
+ // Full URI - return as-is
465
+ return nameOrUri;
466
+ }
467
+ // Short name - build full URI from user's DID
468
+ if (this.userDid) {
469
+ return buildSpaceUri(this.userDid, parsed.name);
470
+ }
471
+ // No user DID available, return as-is
472
+ return nameOrUri;
473
+ }
474
+ // ===========================================================================
475
+ // Exists Check
476
+ // ===========================================================================
477
+ /**
478
+ * Check if a space exists and the user has access.
479
+ */
480
+ async exists(nameOrUri) {
481
+ const spaceId = this.resolveSpaceId(nameOrUri);
482
+ // Check info cache first
483
+ const cached = this.infoCache.get(spaceId);
484
+ if (cached && Date.now() - cached.cachedAt < this.cacheTTL) {
485
+ return ok(true);
486
+ }
487
+ // Query the server
488
+ const infoResult = await this.getSpaceInfo(spaceId);
489
+ return ok(infoResult.ok);
490
+ }
491
+ // ===========================================================================
492
+ // Space Info
493
+ // ===========================================================================
494
+ /**
495
+ * Get space info from server or cache.
496
+ */
497
+ async getSpaceInfo(spaceId) {
498
+ // Check cache
499
+ const cached = this.infoCache.get(spaceId);
500
+ if (cached && Date.now() - cached.cachedAt < this.cacheTTL) {
501
+ return ok(cached.info);
502
+ }
503
+ if (!this.session) {
504
+ return err(serviceError(SpaceErrorCodes.AUTH_REQUIRED, "Authentication required", SERVICE_NAME));
505
+ }
506
+ try {
507
+ const headers = this.invoke(this.session, "space", spaceId, "tinycloud.space/info");
508
+ const response = await this.fetchFn(`${this.host}/invoke`, {
509
+ method: "POST",
510
+ headers,
511
+ body: JSON.stringify({ spaceId }),
512
+ });
513
+ if (!response.ok) {
514
+ if (response.status === 404) {
515
+ return err(serviceError(SpaceErrorCodes.NOT_FOUND, `Space not found: ${spaceId}`, SERVICE_NAME));
516
+ }
517
+ const errorText = await response.text();
518
+ return err(serviceError(SpaceErrorCodes.NETWORK_ERROR, `Failed to get space info: ${response.status} - ${errorText}`, SERVICE_NAME));
519
+ }
520
+ const rawData = await response.json();
521
+ // Validate server response
522
+ const validationResult = validateServerSpaceInfoResponse(rawData);
523
+ if (!validationResult.ok) {
524
+ return err(serviceError(SpaceErrorCodes.NETWORK_ERROR, validationResult.error.message, SERVICE_NAME, { meta: validationResult.error.meta }));
525
+ }
526
+ const data = validationResult.data;
527
+ const spaceInfo = {
528
+ id: data.id,
529
+ name: data.name ?? this.extractNameFromId(data.id),
530
+ owner: data.owner,
531
+ type: data.type ?? (data.owner === this.userDid ? "owned" : "delegated"),
532
+ permissions: data.permissions,
533
+ expiresAt: data.expiresAt ? new Date(data.expiresAt) : undefined,
534
+ };
535
+ // Cache the info
536
+ this.infoCache.set(spaceId, { info: spaceInfo, cachedAt: Date.now() });
537
+ return ok(spaceInfo);
538
+ }
539
+ catch (error) {
540
+ return err(serviceError(SpaceErrorCodes.NETWORK_ERROR, `Network error getting space info: ${String(error)}`, SERVICE_NAME, { cause: error instanceof Error ? error : undefined }));
541
+ }
542
+ }
543
+ // ===========================================================================
544
+ // Space-Scoped Service Factories
545
+ // ===========================================================================
546
+ /**
547
+ * Create a space-scoped KV service.
548
+ */
549
+ createSpaceScopedKV(spaceId) {
550
+ if (this.createKVServiceFn) {
551
+ return this.createKVServiceFn(spaceId);
552
+ }
553
+ // Return a proxy that throws a helpful error
554
+ return new Proxy({}, {
555
+ get: () => {
556
+ throw new Error("KV service factory not configured. Provide createKVService in SpaceServiceConfig.");
557
+ },
558
+ });
559
+ }
560
+ /**
561
+ * Create space-scoped delegation operations.
562
+ */
563
+ createSpaceScopedDelegations(spaceId) {
564
+ const self = this;
565
+ return {
566
+ async list() {
567
+ // List outgoing delegations (created by user) using tinycloud.capabilities/read
568
+ try {
569
+ // Facts contain the query params for the capabilities/read capability
570
+ const facts = [
571
+ {
572
+ capabilitiesReadParams: {
573
+ type: "list",
574
+ filters: { direction: "created" },
575
+ },
576
+ },
577
+ ];
578
+ // The capabilities/read endpoint requires path="all" to match server routing
579
+ const headers = self.invoke(self.session, "capabilities", "all", "tinycloud.capabilities/read", facts);
580
+ const response = await self.fetchFn(`${self.host}/invoke`, {
581
+ method: "POST",
582
+ headers,
583
+ });
584
+ if (!response.ok) {
585
+ const errorText = await response.text();
586
+ return err(serviceError(SpaceErrorCodes.NETWORK_ERROR, `Failed to list delegations: ${response.status} - ${errorText}`, SERVICE_NAME));
587
+ }
588
+ // Server returns { [cid: string]: DelegationInfo } - validate and transform to Delegation[]
589
+ const rawData = await response.json();
590
+ // Validate server response
591
+ const validationResult = validateServerDelegationsResponse(rawData);
592
+ if (!validationResult.ok) {
593
+ return err(serviceError(SpaceErrorCodes.NETWORK_ERROR, validationResult.error.message, SERVICE_NAME, { meta: validationResult.error.meta }));
594
+ }
595
+ const delegations = transformServerDelegations(validationResult.data, spaceId);
596
+ return ok(delegations);
597
+ }
598
+ catch (error) {
599
+ return err(serviceError(SpaceErrorCodes.NETWORK_ERROR, `Network error listing delegations: ${String(error)}`, SERVICE_NAME));
600
+ }
601
+ },
602
+ async listReceived() {
603
+ // List incoming delegations (received by user) using tinycloud.capabilities/read
604
+ try {
605
+ // Facts contain the query params for the capabilities/read capability
606
+ const facts = [
607
+ {
608
+ capabilitiesReadParams: {
609
+ type: "list",
610
+ filters: { direction: "received" },
611
+ },
612
+ },
613
+ ];
614
+ // The capabilities/read endpoint requires path="all" to match server routing
615
+ const headers = self.invoke(self.session, "capabilities", "all", "tinycloud.capabilities/read", facts);
616
+ const response = await self.fetchFn(`${self.host}/invoke`, {
617
+ method: "POST",
618
+ headers,
619
+ });
620
+ if (!response.ok) {
621
+ const errorText = await response.text();
622
+ return err(serviceError(SpaceErrorCodes.NETWORK_ERROR, `Failed to list received delegations: ${response.status} - ${errorText}`, SERVICE_NAME));
623
+ }
624
+ // Server returns { [cid: string]: DelegationInfo } - validate and transform to Delegation[]
625
+ const rawData = await response.json();
626
+ // Validate server response
627
+ const validationResult = validateServerDelegationsResponse(rawData);
628
+ if (!validationResult.ok) {
629
+ return err(serviceError(SpaceErrorCodes.NETWORK_ERROR, validationResult.error.message, SERVICE_NAME, { meta: validationResult.error.meta }));
630
+ }
631
+ const delegations = transformServerDelegations(validationResult.data, spaceId);
632
+ return ok(delegations);
633
+ }
634
+ catch (error) {
635
+ return err(serviceError(SpaceErrorCodes.NETWORK_ERROR, `Network error listing received delegations: ${String(error)}`, SERVICE_NAME));
636
+ }
637
+ },
638
+ async create(params) {
639
+ // Use the platform-provided createDelegation function (SIWE-based flow)
640
+ if (self.createDelegationFn) {
641
+ return self.createDelegationFn({ ...params, spaceId });
642
+ }
643
+ // Fallback: return error if no createDelegation function provided
644
+ // The old invoke-based approach doesn't work because /delegate expects
645
+ // SIWE delegation headers, not invocation headers
646
+ return err(serviceError(SpaceErrorCodes.NOT_INITIALIZED, "Delegation creation requires a createDelegation function. " +
647
+ "This should be provided by the platform SDK (web-sdk or node-sdk).", SERVICE_NAME));
648
+ },
649
+ async revoke(cid) {
650
+ try {
651
+ const headers = self.invoke(self.session, "delegation", cid, "tinycloud.delegation/revoke");
652
+ const response = await self.fetchFn(`${self.host}/revoke`, {
653
+ method: "POST",
654
+ headers,
655
+ body: JSON.stringify({ cid, spaceId }),
656
+ });
657
+ if (!response.ok) {
658
+ const errorText = await response.text();
659
+ return err(serviceError(SpaceErrorCodes.NETWORK_ERROR, `Failed to revoke delegation: ${response.status} - ${errorText}`, SERVICE_NAME));
660
+ }
661
+ return ok(undefined);
662
+ }
663
+ catch (error) {
664
+ return err(serviceError(SpaceErrorCodes.NETWORK_ERROR, `Network error revoking delegation: ${String(error)}`, SERVICE_NAME));
665
+ }
666
+ },
667
+ };
668
+ }
669
+ /**
670
+ * Create space-scoped sharing operations.
671
+ *
672
+ * When a SharingService is configured, delegates to client-side v2 sharing.
673
+ * V2 sharing links are self-contained with embedded private keys - no server tracking.
674
+ */
675
+ createSpaceScopedSharing(spaceId) {
676
+ const self = this;
677
+ return {
678
+ async generate(params) {
679
+ // Use v2 SharingService when available (client-side sharing links)
680
+ if (self.sharingService) {
681
+ // Note: SharingService uses session.spaceId for the space.
682
+ // For space-scoped sharing on delegated spaces, ensure the session
683
+ // is configured for the correct space before calling generate().
684
+ const result = await self.sharingService.generate(params);
685
+ if (!result.ok) {
686
+ return err(serviceError(SpaceErrorCodes.NETWORK_ERROR, result.error.message || "Failed to generate share link", SERVICE_NAME));
687
+ }
688
+ return ok(result.data);
689
+ }
690
+ // Fallback: return error since server endpoint doesn't exist
691
+ return err(serviceError(SpaceErrorCodes.NOT_INITIALIZED, "SharingService not configured. V2 sharing requires a SharingService instance.", SERVICE_NAME));
692
+ },
693
+ async list() {
694
+ // V2 sharing links are self-contained (not server-tracked)
695
+ // This operation is not supported in the v2 spec
696
+ return err(serviceError(SpaceErrorCodes.NOT_INITIALIZED, "Listing share links is not supported in v2. Share links are self-contained tokens that are not tracked on the server.", SERVICE_NAME));
697
+ },
698
+ async revoke(token) {
699
+ // V2 sharing links are revoked by revoking the underlying delegation
700
+ // This requires the delegation CID, not the share token
701
+ return err(serviceError(SpaceErrorCodes.NOT_INITIALIZED, "Revoking share links by token is not supported in v2. To revoke access, revoke the underlying delegation using space.delegations.revoke(cid).", SERVICE_NAME));
702
+ },
703
+ };
704
+ }
705
+ }
706
+ /**
707
+ * Create a new SpaceService instance.
708
+ *
709
+ * @param config - Service configuration
710
+ * @returns A new SpaceService instance
711
+ */
712
+ export function createSpaceService(config) {
713
+ return new SpaceService(config);
714
+ }
715
+ //# sourceMappingURL=SpaceService.js.map