@tinycloudlabs/node-sdk 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.
- package/LICENSE.md +320 -0
- package/dist/DelegatedAccess.d.ts +33 -0
- package/dist/DelegatedAccess.d.ts.map +1 -0
- package/dist/DelegatedAccess.js +61 -0
- package/dist/DelegatedAccess.js.map +1 -0
- package/dist/TinyCloudNode.d.ts +441 -0
- package/dist/TinyCloudNode.d.ts.map +1 -0
- package/dist/TinyCloudNode.js +987 -0
- package/dist/TinyCloudNode.js.map +1 -0
- package/dist/authorization/NodeUserAuthorization.d.ts +200 -0
- package/dist/authorization/NodeUserAuthorization.d.ts.map +1 -0
- package/dist/authorization/NodeUserAuthorization.js +516 -0
- package/dist/authorization/NodeUserAuthorization.js.map +1 -0
- package/dist/authorization/strategies.d.ts +57 -0
- package/dist/authorization/strategies.d.ts.map +1 -0
- package/dist/authorization/strategies.js +15 -0
- package/dist/authorization/strategies.js.map +1 -0
- package/dist/delegation.d.ts +35 -0
- package/dist/delegation.d.ts.map +1 -0
- package/dist/delegation.js +21 -0
- package/dist/delegation.js.map +1 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +73 -0
- package/dist/index.js.map +1 -0
- package/dist/keys/WasmKeyProvider.d.ts +101 -0
- package/dist/keys/WasmKeyProvider.d.ts.map +1 -0
- package/dist/keys/WasmKeyProvider.js +113 -0
- package/dist/keys/WasmKeyProvider.js.map +1 -0
- package/dist/keys/index.d.ts +7 -0
- package/dist/keys/index.d.ts.map +1 -0
- package/dist/keys/index.js +7 -0
- package/dist/keys/index.js.map +1 -0
- package/dist/signers/PrivateKeySigner.d.ts +47 -0
- package/dist/signers/PrivateKeySigner.d.ts.map +1 -0
- package/dist/signers/PrivateKeySigner.js +89 -0
- package/dist/signers/PrivateKeySigner.js.map +1 -0
- package/dist/storage/FileSessionStorage.d.ts +59 -0
- package/dist/storage/FileSessionStorage.d.ts.map +1 -0
- package/dist/storage/FileSessionStorage.js +148 -0
- package/dist/storage/FileSessionStorage.js.map +1 -0
- package/dist/storage/MemorySessionStorage.d.ts +49 -0
- package/dist/storage/MemorySessionStorage.d.ts.map +1 -0
- package/dist/storage/MemorySessionStorage.js +88 -0
- package/dist/storage/MemorySessionStorage.js.map +1 -0
- package/package.json +43 -0
|
@@ -0,0 +1,987 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TinyCloudNode - High-level API for Node.js users.
|
|
3
|
+
*
|
|
4
|
+
* Each user has their own TinyCloudNode instance with their own key.
|
|
5
|
+
* This class provides a simplified interface for:
|
|
6
|
+
* - Signing in and managing sessions
|
|
7
|
+
* - Key-value storage operations on own space
|
|
8
|
+
* - Creating and using delegations
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* const alice = new TinyCloudNode({
|
|
13
|
+
* privateKey: process.env.ALICE_PRIVATE_KEY,
|
|
14
|
+
* host: "https://node.tinycloud.xyz",
|
|
15
|
+
* prefix: "myapp",
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* await alice.signIn();
|
|
19
|
+
* await alice.kv.put("greeting", "Hello, world!");
|
|
20
|
+
*
|
|
21
|
+
* // Delegate access to Bob
|
|
22
|
+
* const delegation = await alice.createDelegation({
|
|
23
|
+
* path: "shared/",
|
|
24
|
+
* actions: ["tinycloud.kv/get", "tinycloud.kv/put"],
|
|
25
|
+
* delegateDID: bob.did,
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* // Bob uses the delegation
|
|
29
|
+
* const access = await bob.useDelegation(delegation);
|
|
30
|
+
* const data = await access.kv.get("shared/data");
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
import { TinyCloud, activateSessionWithHost, KVService, ServiceContext,
|
|
34
|
+
// v2 services
|
|
35
|
+
DelegationManager, SpaceService, CapabilityKeyRegistry, SharingService, } from "@tinycloudlabs/sdk-core";
|
|
36
|
+
import { NodeUserAuthorization } from "./authorization/NodeUserAuthorization";
|
|
37
|
+
import { PrivateKeySigner } from "./signers/PrivateKeySigner";
|
|
38
|
+
import { MemorySessionStorage } from "./storage/MemorySessionStorage";
|
|
39
|
+
import { TCWSessionManager as SessionManager, prepareSession, completeSessionSetup, ensureEip55, invoke, initPanicHook, createDelegation, } from "@tinycloudlabs/node-sdk-wasm";
|
|
40
|
+
import { DelegatedAccess } from "./DelegatedAccess";
|
|
41
|
+
import { WasmKeyProvider } from "./keys/WasmKeyProvider";
|
|
42
|
+
/** Default TinyCloud host */
|
|
43
|
+
const DEFAULT_HOST = "https://node.tinycloud.xyz";
|
|
44
|
+
/**
|
|
45
|
+
* High-level TinyCloud API for Node.js environments.
|
|
46
|
+
*
|
|
47
|
+
* Each user creates their own TinyCloudNode instance with their private key.
|
|
48
|
+
* The instance manages the user's session and provides access to their space.
|
|
49
|
+
*/
|
|
50
|
+
export class TinyCloudNode {
|
|
51
|
+
/**
|
|
52
|
+
* Create a new TinyCloudNode instance.
|
|
53
|
+
*
|
|
54
|
+
* All configuration is optional. Without a privateKey, the instance operates
|
|
55
|
+
* in "session-only" mode where it can receive delegations but cannot create
|
|
56
|
+
* its own space via signIn().
|
|
57
|
+
*
|
|
58
|
+
* @param config - Configuration options (all optional)
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```typescript
|
|
62
|
+
* // Session-only mode - can receive delegations
|
|
63
|
+
* const bob = new TinyCloudNode();
|
|
64
|
+
* console.log(bob.did); // did:key:z6Mk... - available immediately
|
|
65
|
+
*
|
|
66
|
+
* // Wallet mode - can create own space
|
|
67
|
+
* const alice = new TinyCloudNode({
|
|
68
|
+
* privateKey: process.env.ALICE_PRIVATE_KEY,
|
|
69
|
+
* prefix: "myapp",
|
|
70
|
+
* });
|
|
71
|
+
* await alice.signIn();
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
constructor(config = {}) {
|
|
75
|
+
this.signer = null;
|
|
76
|
+
this.auth = null;
|
|
77
|
+
this.tc = null;
|
|
78
|
+
this._chainId = 1;
|
|
79
|
+
// Initialize WASM panic hook once
|
|
80
|
+
if (!TinyCloudNode.wasmInitialized) {
|
|
81
|
+
initPanicHook();
|
|
82
|
+
TinyCloudNode.wasmInitialized = true;
|
|
83
|
+
}
|
|
84
|
+
// Store config with default host
|
|
85
|
+
this.config = {
|
|
86
|
+
...config,
|
|
87
|
+
host: config.host ?? DEFAULT_HOST,
|
|
88
|
+
};
|
|
89
|
+
// Always create session manager and session key immediately
|
|
90
|
+
this.sessionManager = new SessionManager();
|
|
91
|
+
// Try to use "default" key, create if it doesn't exist
|
|
92
|
+
const defaultKeyId = "default";
|
|
93
|
+
let jwkStr = this.sessionManager.jwk(defaultKeyId);
|
|
94
|
+
if (jwkStr) {
|
|
95
|
+
// Key already exists, reuse it
|
|
96
|
+
this.sessionKeyId = defaultKeyId;
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
// Create new key
|
|
100
|
+
this.sessionKeyId = this.sessionManager.createSessionKey(defaultKeyId);
|
|
101
|
+
jwkStr = this.sessionManager.jwk(this.sessionKeyId);
|
|
102
|
+
}
|
|
103
|
+
if (!jwkStr) {
|
|
104
|
+
throw new Error("Failed to get session key JWK");
|
|
105
|
+
}
|
|
106
|
+
this.sessionKeyJwk = JSON.parse(jwkStr);
|
|
107
|
+
// Initialize capability registry for all users (needed for tracking received delegations)
|
|
108
|
+
this._capabilityRegistry = new CapabilityKeyRegistry();
|
|
109
|
+
// Initialize KeyProvider for SharingService
|
|
110
|
+
this._keyProvider = new WasmKeyProvider({
|
|
111
|
+
sessionManager: this.sessionManager,
|
|
112
|
+
});
|
|
113
|
+
// Initialize SharingService for receive-only access (no session required)
|
|
114
|
+
// This allows session-only users to receive sharing links without signIn()
|
|
115
|
+
// Full capabilities (generate) are added after signIn()
|
|
116
|
+
this._sharingService = new SharingService({
|
|
117
|
+
hosts: [this.config.host],
|
|
118
|
+
// session: undefined - not needed for receive()
|
|
119
|
+
invoke,
|
|
120
|
+
fetch: globalThis.fetch.bind(globalThis),
|
|
121
|
+
keyProvider: this._keyProvider,
|
|
122
|
+
registry: this._capabilityRegistry,
|
|
123
|
+
// delegationManager: undefined - not needed for receive()
|
|
124
|
+
createKVService: (config) => {
|
|
125
|
+
// Use pathPrefix as the KV service prefix for sharing links
|
|
126
|
+
// Strip trailing slash to match DelegatedAccess behavior
|
|
127
|
+
const prefix = config.pathPrefix?.replace(/\/$/, '');
|
|
128
|
+
const kvService = new KVService({ prefix });
|
|
129
|
+
// Create a new service context for the KV service
|
|
130
|
+
const kvContext = new ServiceContext({
|
|
131
|
+
invoke: config.invoke,
|
|
132
|
+
fetch: config.fetch ?? globalThis.fetch.bind(globalThis),
|
|
133
|
+
hosts: config.hosts,
|
|
134
|
+
});
|
|
135
|
+
kvContext.setSession(config.session);
|
|
136
|
+
kvService.initialize(kvContext);
|
|
137
|
+
return kvService;
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
// Only set up wallet/auth if privateKey is provided
|
|
141
|
+
if (config.privateKey) {
|
|
142
|
+
this.signer = new PrivateKeySigner(config.privateKey, this._chainId);
|
|
143
|
+
// Derive domain from host if not provided
|
|
144
|
+
const host = this.config.host;
|
|
145
|
+
const domain = config.domain ?? new URL(host).hostname;
|
|
146
|
+
this.auth = new NodeUserAuthorization({
|
|
147
|
+
signer: this.signer,
|
|
148
|
+
signStrategy: { type: "auto-sign" },
|
|
149
|
+
sessionStorage: new MemorySessionStorage(),
|
|
150
|
+
domain,
|
|
151
|
+
spacePrefix: config.prefix,
|
|
152
|
+
sessionExpirationMs: config.sessionExpirationMs ?? 60 * 60 * 1000,
|
|
153
|
+
tinycloudHosts: [host],
|
|
154
|
+
autoCreateSpace: config.autoCreateSpace,
|
|
155
|
+
});
|
|
156
|
+
this.tc = new TinyCloud(this.auth);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Get the primary identity DID for this user.
|
|
161
|
+
* - If wallet connected and signed in: returns PKH DID (did:pkh:eip155:{chainId}:{address})
|
|
162
|
+
* - If session-only mode: returns session key DID (did:key:z6Mk...)
|
|
163
|
+
*
|
|
164
|
+
* Use this for delegations - it always returns the appropriate identity.
|
|
165
|
+
*/
|
|
166
|
+
get did() {
|
|
167
|
+
// If wallet is connected and signed in, return PKH (persistent identity)
|
|
168
|
+
if (this._address) {
|
|
169
|
+
return `did:pkh:eip155:${this._chainId}:${this._address}`;
|
|
170
|
+
}
|
|
171
|
+
// Session-only mode: return session key DID (ephemeral identity)
|
|
172
|
+
return this.sessionManager.getDID(this.sessionKeyId);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Get the session key DID. Always available.
|
|
176
|
+
* Format: did:key:z6Mk...#z6Mk...
|
|
177
|
+
*
|
|
178
|
+
* Use this when you specifically need the session key, not the user identity.
|
|
179
|
+
*/
|
|
180
|
+
get sessionDid() {
|
|
181
|
+
return this.sessionManager.getDID(this.sessionKeyId);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Get the Ethereum address for this user.
|
|
185
|
+
*/
|
|
186
|
+
get address() {
|
|
187
|
+
return this._address;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Check if this instance is in session-only mode (no wallet).
|
|
191
|
+
* In session-only mode, the instance can receive delegations but cannot
|
|
192
|
+
* create its own space via signIn().
|
|
193
|
+
*/
|
|
194
|
+
get isSessionOnly() {
|
|
195
|
+
return this.signer === null;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Get the space ID for this user.
|
|
199
|
+
* Available after signIn().
|
|
200
|
+
*/
|
|
201
|
+
get spaceId() {
|
|
202
|
+
return this.auth?.tinyCloudSession?.spaceId;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Get the current TinyCloud session.
|
|
206
|
+
* Available after signIn().
|
|
207
|
+
*/
|
|
208
|
+
get session() {
|
|
209
|
+
return this.auth?.tinyCloudSession;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Sign in and create a new session.
|
|
213
|
+
* This creates the user's space if it doesn't exist.
|
|
214
|
+
* Requires wallet mode (privateKey in config).
|
|
215
|
+
*/
|
|
216
|
+
async signIn() {
|
|
217
|
+
if (!this.signer || !this.tc) {
|
|
218
|
+
throw new Error("Cannot signIn() in session-only mode. Provide a privateKey in config to create your own space.");
|
|
219
|
+
}
|
|
220
|
+
this._address = await this.signer.getAddress();
|
|
221
|
+
this._chainId = await this.signer.getChainId();
|
|
222
|
+
// Reset KV service so it gets recreated with new session
|
|
223
|
+
this._kv = undefined;
|
|
224
|
+
this._serviceContext = undefined;
|
|
225
|
+
await this.tc.signIn();
|
|
226
|
+
// Initialize service context with session
|
|
227
|
+
this.initializeServices();
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Connect a wallet to upgrade from session-only mode to wallet mode.
|
|
231
|
+
*
|
|
232
|
+
* This allows a user who started in session-only mode to later connect
|
|
233
|
+
* a wallet and gain the ability to create their own space.
|
|
234
|
+
*
|
|
235
|
+
* Note: This does NOT automatically sign in. Call signIn() after connecting
|
|
236
|
+
* the wallet to create your space.
|
|
237
|
+
*
|
|
238
|
+
* @param privateKey - The Ethereum private key (hex string, no 0x prefix)
|
|
239
|
+
* @param options - Optional configuration
|
|
240
|
+
* @param options.prefix - Space name prefix (defaults to "default")
|
|
241
|
+
*
|
|
242
|
+
* @example
|
|
243
|
+
* ```typescript
|
|
244
|
+
* // Start in session-only mode
|
|
245
|
+
* const node = new TinyCloudNode({ host: "https://node.tinycloud.xyz" });
|
|
246
|
+
* console.log(node.did); // did:key:z6Mk... (session key)
|
|
247
|
+
*
|
|
248
|
+
* // Later, connect a wallet
|
|
249
|
+
* node.connectWallet(privateKey);
|
|
250
|
+
* await node.signIn();
|
|
251
|
+
* console.log(node.did); // did:pkh:eip155:1:0x... (PKH)
|
|
252
|
+
* ```
|
|
253
|
+
*/
|
|
254
|
+
connectWallet(privateKey, options) {
|
|
255
|
+
if (this.signer) {
|
|
256
|
+
throw new Error("Wallet already connected. Cannot connect another wallet.");
|
|
257
|
+
}
|
|
258
|
+
const prefix = options?.prefix ?? "default";
|
|
259
|
+
const host = this.config.host;
|
|
260
|
+
const domain = new URL(host).hostname;
|
|
261
|
+
// Create signer from private key
|
|
262
|
+
this.signer = new PrivateKeySigner(privateKey);
|
|
263
|
+
// Create authorization handler
|
|
264
|
+
this.auth = new NodeUserAuthorization({
|
|
265
|
+
signer: this.signer,
|
|
266
|
+
signStrategy: { type: "auto-sign" },
|
|
267
|
+
sessionStorage: new MemorySessionStorage(),
|
|
268
|
+
domain,
|
|
269
|
+
spacePrefix: prefix,
|
|
270
|
+
sessionExpirationMs: this.config.sessionExpirationMs ?? 60 * 60 * 1000,
|
|
271
|
+
tinycloudHosts: [host],
|
|
272
|
+
autoCreateSpace: this.config.autoCreateSpace,
|
|
273
|
+
});
|
|
274
|
+
// Create TinyCloud instance
|
|
275
|
+
this.tc = new TinyCloud(this.auth);
|
|
276
|
+
// Update config with prefix
|
|
277
|
+
this.config.prefix = prefix;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Initialize the service context and KV service after sign-in.
|
|
281
|
+
* @internal
|
|
282
|
+
*/
|
|
283
|
+
initializeServices() {
|
|
284
|
+
const session = this.auth?.tinyCloudSession;
|
|
285
|
+
if (!session) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
// Create service context
|
|
289
|
+
this._serviceContext = new ServiceContext({
|
|
290
|
+
invoke,
|
|
291
|
+
fetch: globalThis.fetch.bind(globalThis),
|
|
292
|
+
hosts: [this.config.host],
|
|
293
|
+
});
|
|
294
|
+
// Create and register KV service
|
|
295
|
+
this._kv = new KVService({});
|
|
296
|
+
this._kv.initialize(this._serviceContext);
|
|
297
|
+
this._serviceContext.registerService('kv', this._kv);
|
|
298
|
+
// Set session on context
|
|
299
|
+
const serviceSession = {
|
|
300
|
+
delegationHeader: session.delegationHeader,
|
|
301
|
+
delegationCid: session.delegationCid,
|
|
302
|
+
spaceId: session.spaceId,
|
|
303
|
+
verificationMethod: session.verificationMethod,
|
|
304
|
+
jwk: session.jwk,
|
|
305
|
+
};
|
|
306
|
+
this._serviceContext.setSession(serviceSession);
|
|
307
|
+
// Initialize v2 services
|
|
308
|
+
this.initializeV2Services(serviceSession);
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Initialize the v2 delegation system services.
|
|
312
|
+
* @internal
|
|
313
|
+
*/
|
|
314
|
+
initializeV2Services(serviceSession) {
|
|
315
|
+
// Initialize CapabilityKeyRegistry
|
|
316
|
+
this._capabilityRegistry = new CapabilityKeyRegistry();
|
|
317
|
+
const tcSession = this.auth?.tinyCloudSession;
|
|
318
|
+
// Register the session key with its capabilities
|
|
319
|
+
if (tcSession && this._address) {
|
|
320
|
+
const sessionKey = {
|
|
321
|
+
id: tcSession.sessionKey,
|
|
322
|
+
did: tcSession.verificationMethod,
|
|
323
|
+
type: "session",
|
|
324
|
+
// Cast jwk from generic object to JWK - we know it has the required structure
|
|
325
|
+
jwk: tcSession.jwk,
|
|
326
|
+
priority: 0, // Session keys have highest priority
|
|
327
|
+
};
|
|
328
|
+
// Create root delegation for the session
|
|
329
|
+
const rootDelegation = {
|
|
330
|
+
cid: tcSession.delegationCid,
|
|
331
|
+
delegateDID: tcSession.verificationMethod,
|
|
332
|
+
spaceId: tcSession.spaceId,
|
|
333
|
+
path: "", // Root access
|
|
334
|
+
actions: [
|
|
335
|
+
"tinycloud.kv/put",
|
|
336
|
+
"tinycloud.kv/get",
|
|
337
|
+
"tinycloud.kv/del",
|
|
338
|
+
"tinycloud.kv/list",
|
|
339
|
+
"tinycloud.kv/metadata",
|
|
340
|
+
],
|
|
341
|
+
expiry: this.getSessionExpiry(),
|
|
342
|
+
isRevoked: false,
|
|
343
|
+
allowSubDelegation: true,
|
|
344
|
+
};
|
|
345
|
+
this._capabilityRegistry.registerKey(sessionKey, [rootDelegation]);
|
|
346
|
+
}
|
|
347
|
+
// Initialize DelegationManager
|
|
348
|
+
this._delegationManager = new DelegationManager({
|
|
349
|
+
hosts: [this.config.host],
|
|
350
|
+
session: serviceSession,
|
|
351
|
+
invoke,
|
|
352
|
+
fetch: globalThis.fetch.bind(globalThis),
|
|
353
|
+
});
|
|
354
|
+
// Initialize SpaceService
|
|
355
|
+
this._spaceService = new SpaceService({
|
|
356
|
+
hosts: [this.config.host],
|
|
357
|
+
session: serviceSession,
|
|
358
|
+
invoke,
|
|
359
|
+
fetch: globalThis.fetch.bind(globalThis),
|
|
360
|
+
capabilityRegistry: this._capabilityRegistry,
|
|
361
|
+
userDid: this.did,
|
|
362
|
+
createKVService: (spaceId) => {
|
|
363
|
+
// Create a new KV service scoped to the specified space
|
|
364
|
+
const kvService = new KVService({});
|
|
365
|
+
if (this._serviceContext) {
|
|
366
|
+
kvService.initialize(this._serviceContext);
|
|
367
|
+
}
|
|
368
|
+
return kvService;
|
|
369
|
+
},
|
|
370
|
+
// Enable space.delegations.create() via SIWE-based delegation
|
|
371
|
+
createDelegation: async (params) => {
|
|
372
|
+
try {
|
|
373
|
+
// Use the existing createDelegation method which calls /delegate with SIWE
|
|
374
|
+
const portableDelegation = await this.createDelegation({
|
|
375
|
+
delegateDID: params.delegateDID,
|
|
376
|
+
path: params.path,
|
|
377
|
+
actions: params.actions,
|
|
378
|
+
disableSubDelegation: params.disableSubDelegation,
|
|
379
|
+
expiryMs: params.expiry
|
|
380
|
+
? params.expiry.getTime() - Date.now()
|
|
381
|
+
: undefined,
|
|
382
|
+
});
|
|
383
|
+
// Convert PortableDelegation to Delegation type for Space API
|
|
384
|
+
const delegation = {
|
|
385
|
+
cid: portableDelegation.cid,
|
|
386
|
+
delegateDID: portableDelegation.delegateDID,
|
|
387
|
+
delegatorDID: this.did,
|
|
388
|
+
spaceId: portableDelegation.spaceId,
|
|
389
|
+
path: portableDelegation.path,
|
|
390
|
+
actions: portableDelegation.actions,
|
|
391
|
+
expiry: portableDelegation.expiry,
|
|
392
|
+
isRevoked: false,
|
|
393
|
+
allowSubDelegation: !portableDelegation.disableSubDelegation,
|
|
394
|
+
createdAt: new Date(),
|
|
395
|
+
authHeader: portableDelegation.delegationHeader.Authorization,
|
|
396
|
+
};
|
|
397
|
+
return { ok: true, data: delegation };
|
|
398
|
+
}
|
|
399
|
+
catch (error) {
|
|
400
|
+
return {
|
|
401
|
+
ok: false,
|
|
402
|
+
error: {
|
|
403
|
+
code: "CREATION_FAILED",
|
|
404
|
+
message: error instanceof Error ? error.message : String(error),
|
|
405
|
+
service: "delegation",
|
|
406
|
+
},
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
});
|
|
411
|
+
// Update SharingService with full capabilities (session + createDelegation)
|
|
412
|
+
// SharingService was initialized in constructor for receive-only access
|
|
413
|
+
this._sharingService.updateConfig({
|
|
414
|
+
session: serviceSession,
|
|
415
|
+
delegationManager: this._delegationManager,
|
|
416
|
+
sessionExpiry: this.getSessionExpiry(),
|
|
417
|
+
// WASM-based delegation creation (preferred - no server roundtrip)
|
|
418
|
+
createDelegationWasm: (params) => this.createDelegationWrapper(params),
|
|
419
|
+
});
|
|
420
|
+
// Wire up SharingService to SpaceService for space.sharing.generate()
|
|
421
|
+
this._spaceService.updateConfig({
|
|
422
|
+
sharingService: this._sharingService,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Get the session expiry time.
|
|
427
|
+
* @internal
|
|
428
|
+
*/
|
|
429
|
+
getSessionExpiry() {
|
|
430
|
+
// Default to 1 hour from now if not explicitly set
|
|
431
|
+
const expirationMs = this.config.sessionExpirationMs ?? 60 * 60 * 1000;
|
|
432
|
+
return new Date(Date.now() + expirationMs);
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Wrapper for the WASM createDelegation function.
|
|
436
|
+
* Adapts the WASM interface to what SharingService expects.
|
|
437
|
+
* @internal
|
|
438
|
+
*/
|
|
439
|
+
createDelegationWrapper(params) {
|
|
440
|
+
// Convert ServiceSession to the format WASM expects
|
|
441
|
+
const wasmSession = {
|
|
442
|
+
delegationHeader: params.session.delegationHeader,
|
|
443
|
+
delegationCid: params.session.delegationCid,
|
|
444
|
+
jwk: params.session.jwk,
|
|
445
|
+
spaceId: params.session.spaceId,
|
|
446
|
+
verificationMethod: params.session.verificationMethod,
|
|
447
|
+
};
|
|
448
|
+
const result = createDelegation(wasmSession, params.delegateDID, params.spaceId, params.path, params.actions, params.expirationSecs, params.notBeforeSecs);
|
|
449
|
+
return {
|
|
450
|
+
delegation: result.delegation,
|
|
451
|
+
cid: result.cid,
|
|
452
|
+
delegateDID: result.delegateDid,
|
|
453
|
+
path: result.path,
|
|
454
|
+
actions: result.actions,
|
|
455
|
+
expiry: new Date(result.expiry * 1000),
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Track a received delegation in the capability registry.
|
|
460
|
+
* @internal
|
|
461
|
+
*/
|
|
462
|
+
trackReceivedDelegation(delegation, jwk) {
|
|
463
|
+
if (!this._capabilityRegistry) {
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
const keyInfo = {
|
|
467
|
+
id: `received:${delegation.cid}`,
|
|
468
|
+
did: this.sessionDid,
|
|
469
|
+
type: "ingested",
|
|
470
|
+
jwk,
|
|
471
|
+
priority: 2,
|
|
472
|
+
};
|
|
473
|
+
// Convert PortableDelegation to Delegation type
|
|
474
|
+
const delegationRecord = {
|
|
475
|
+
cid: delegation.cid,
|
|
476
|
+
delegateDID: delegation.delegateDID,
|
|
477
|
+
spaceId: delegation.spaceId,
|
|
478
|
+
path: delegation.path,
|
|
479
|
+
actions: delegation.actions,
|
|
480
|
+
expiry: delegation.expiry,
|
|
481
|
+
isRevoked: false,
|
|
482
|
+
allowSubDelegation: !delegation.disableSubDelegation,
|
|
483
|
+
};
|
|
484
|
+
this._capabilityRegistry.ingestKey(keyInfo, delegationRecord);
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Key-value storage operations on this user's space.
|
|
488
|
+
*/
|
|
489
|
+
get kv() {
|
|
490
|
+
if (!this._kv) {
|
|
491
|
+
throw new Error("Not signed in. Call signIn() first.");
|
|
492
|
+
}
|
|
493
|
+
return this._kv;
|
|
494
|
+
}
|
|
495
|
+
// ===========================================================================
|
|
496
|
+
// v2 Service Accessors
|
|
497
|
+
// ===========================================================================
|
|
498
|
+
/**
|
|
499
|
+
* Get the CapabilityKeyRegistry for managing keys and their capabilities.
|
|
500
|
+
*
|
|
501
|
+
* The registry tracks keys (session, main, ingested) and their associated
|
|
502
|
+
* delegations, enabling automatic key selection for operations.
|
|
503
|
+
*
|
|
504
|
+
* @example
|
|
505
|
+
* ```typescript
|
|
506
|
+
* const registry = alice.capabilityRegistry;
|
|
507
|
+
*
|
|
508
|
+
* // Get the best key for an operation
|
|
509
|
+
* const key = registry.getKeyForCapability(
|
|
510
|
+
* "tinycloud://my-space/kv/data",
|
|
511
|
+
* "tinycloud.kv/get"
|
|
512
|
+
* );
|
|
513
|
+
*
|
|
514
|
+
* // List all capabilities
|
|
515
|
+
* const capabilities = registry.getAllCapabilities();
|
|
516
|
+
* ```
|
|
517
|
+
*/
|
|
518
|
+
get capabilityRegistry() {
|
|
519
|
+
if (!this._capabilityRegistry) {
|
|
520
|
+
throw new Error("CapabilityKeyRegistry not initialized.");
|
|
521
|
+
}
|
|
522
|
+
return this._capabilityRegistry;
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Access received delegations (recipient view).
|
|
526
|
+
*
|
|
527
|
+
* Use this to see what delegations have been received via useDelegation().
|
|
528
|
+
*
|
|
529
|
+
* @example
|
|
530
|
+
* ```typescript
|
|
531
|
+
* // List all received delegations
|
|
532
|
+
* const received = bob.delegations.list();
|
|
533
|
+
* console.log("I have access to:", received.length, "spaces");
|
|
534
|
+
*
|
|
535
|
+
* // Get a specific delegation by CID
|
|
536
|
+
* const delegation = bob.delegations.get(cid);
|
|
537
|
+
* ```
|
|
538
|
+
*/
|
|
539
|
+
get delegations() {
|
|
540
|
+
const registry = this._capabilityRegistry;
|
|
541
|
+
if (!registry) {
|
|
542
|
+
return {
|
|
543
|
+
list: () => [],
|
|
544
|
+
get: () => undefined,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
return {
|
|
548
|
+
list: () => registry.getAllCapabilities().map((entry) => entry.delegation),
|
|
549
|
+
get: (cid) => {
|
|
550
|
+
const capabilities = registry.getAllCapabilities();
|
|
551
|
+
const entry = capabilities.find((e) => e.delegation.cid === cid);
|
|
552
|
+
return entry?.delegation;
|
|
553
|
+
},
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Get the DelegationManager for delegation CRUD operations.
|
|
558
|
+
*
|
|
559
|
+
* This is the v2 delegation service providing a cleaner API than
|
|
560
|
+
* the legacy createDelegation/useDelegation methods.
|
|
561
|
+
*
|
|
562
|
+
* @example
|
|
563
|
+
* ```typescript
|
|
564
|
+
* const delegations = alice.delegationManager;
|
|
565
|
+
*
|
|
566
|
+
* // Create a delegation
|
|
567
|
+
* const result = await delegations.create({
|
|
568
|
+
* delegateDID: bob.did,
|
|
569
|
+
* path: "shared/",
|
|
570
|
+
* actions: ["tinycloud.kv/get", "tinycloud.kv/put"],
|
|
571
|
+
* expiry: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 hours
|
|
572
|
+
* });
|
|
573
|
+
*
|
|
574
|
+
* // List delegations
|
|
575
|
+
* const listResult = await delegations.list();
|
|
576
|
+
*
|
|
577
|
+
* // Revoke a delegation
|
|
578
|
+
* await delegations.revoke(delegationCid);
|
|
579
|
+
* ```
|
|
580
|
+
*/
|
|
581
|
+
get delegationManager() {
|
|
582
|
+
if (!this._delegationManager) {
|
|
583
|
+
throw new Error("Not signed in. Call signIn() first.");
|
|
584
|
+
}
|
|
585
|
+
return this._delegationManager;
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Get the SpaceService for managing spaces.
|
|
589
|
+
*
|
|
590
|
+
* The SpaceService provides access to owned and delegated spaces,
|
|
591
|
+
* including space creation, listing, and scoped operations.
|
|
592
|
+
*
|
|
593
|
+
* @example
|
|
594
|
+
* ```typescript
|
|
595
|
+
* const spaces = alice.spaces;
|
|
596
|
+
*
|
|
597
|
+
* // List all accessible spaces
|
|
598
|
+
* const result = await spaces.list();
|
|
599
|
+
*
|
|
600
|
+
* // Create a new space
|
|
601
|
+
* const createResult = await spaces.create('photos');
|
|
602
|
+
*
|
|
603
|
+
* // Get a space object for operations
|
|
604
|
+
* const mySpace = spaces.get('default');
|
|
605
|
+
* await mySpace.kv.put('key', 'value');
|
|
606
|
+
*
|
|
607
|
+
* // Check if a space exists
|
|
608
|
+
* const exists = await spaces.exists('photos');
|
|
609
|
+
* ```
|
|
610
|
+
*/
|
|
611
|
+
get spaces() {
|
|
612
|
+
if (!this._spaceService) {
|
|
613
|
+
throw new Error("Not signed in. Call signIn() first.");
|
|
614
|
+
}
|
|
615
|
+
return this._spaceService;
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Alias for `spaces` - get the SpaceService.
|
|
619
|
+
* @see spaces
|
|
620
|
+
*/
|
|
621
|
+
get spaceService() {
|
|
622
|
+
return this.spaces;
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Get the SharingService for creating and receiving v2 sharing links.
|
|
626
|
+
*
|
|
627
|
+
* The SharingService creates sharing links with embedded private keys,
|
|
628
|
+
* allowing recipients to exercise delegations without prior session setup.
|
|
629
|
+
*
|
|
630
|
+
* @example
|
|
631
|
+
* ```typescript
|
|
632
|
+
* const sharing = alice.sharing;
|
|
633
|
+
*
|
|
634
|
+
* // Generate a sharing link
|
|
635
|
+
* const result = await sharing.generate({
|
|
636
|
+
* path: "/kv/documents/report.pdf",
|
|
637
|
+
* actions: ["tinycloud.kv/get"],
|
|
638
|
+
* expiry: new Date(Date.now() + 24 * 60 * 60 * 1000),
|
|
639
|
+
* });
|
|
640
|
+
*
|
|
641
|
+
* if (result.ok) {
|
|
642
|
+
* console.log("Share URL:", result.data.url);
|
|
643
|
+
* // Send the URL to the recipient
|
|
644
|
+
* }
|
|
645
|
+
*
|
|
646
|
+
* // Receive a sharing link
|
|
647
|
+
* const receiveResult = await sharing.receive(shareUrl);
|
|
648
|
+
* if (receiveResult.ok) {
|
|
649
|
+
* // Use the pre-configured KV service
|
|
650
|
+
* const data = await receiveResult.data.kv.get("report.pdf");
|
|
651
|
+
* }
|
|
652
|
+
* ```
|
|
653
|
+
*/
|
|
654
|
+
get sharing() {
|
|
655
|
+
// SharingService is initialized in constructor for receive-only access
|
|
656
|
+
// Full capabilities (generate) are added after signIn()
|
|
657
|
+
return this._sharingService;
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Alias for `sharing` - get the SharingService.
|
|
661
|
+
* @see sharing
|
|
662
|
+
*/
|
|
663
|
+
get sharingService() {
|
|
664
|
+
return this.sharing;
|
|
665
|
+
}
|
|
666
|
+
// ===========================================================================
|
|
667
|
+
// v2 Delegation Convenience Methods
|
|
668
|
+
// ===========================================================================
|
|
669
|
+
/**
|
|
670
|
+
* Create a delegation using the v2 DelegationManager.
|
|
671
|
+
*
|
|
672
|
+
* This is a convenience method that wraps DelegationManager.create().
|
|
673
|
+
* For more control, use `this.delegationManager` directly.
|
|
674
|
+
*
|
|
675
|
+
* @param params - Delegation parameters
|
|
676
|
+
* @returns Result containing the created Delegation
|
|
677
|
+
*
|
|
678
|
+
* @example
|
|
679
|
+
* ```typescript
|
|
680
|
+
* const result = await alice.delegate({
|
|
681
|
+
* delegateDID: bob.did,
|
|
682
|
+
* path: "shared/",
|
|
683
|
+
* actions: ["tinycloud.kv/get", "tinycloud.kv/put"],
|
|
684
|
+
* expiry: new Date(Date.now() + 24 * 60 * 60 * 1000),
|
|
685
|
+
* });
|
|
686
|
+
*
|
|
687
|
+
* if (result.ok) {
|
|
688
|
+
* console.log("Delegation created:", result.data.cid);
|
|
689
|
+
* }
|
|
690
|
+
* ```
|
|
691
|
+
*/
|
|
692
|
+
async delegate(params) {
|
|
693
|
+
return this.delegationManager.create(params);
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Revoke a delegation using the v2 DelegationManager.
|
|
697
|
+
*
|
|
698
|
+
* @param cid - The CID of the delegation to revoke
|
|
699
|
+
* @returns Result indicating success or failure
|
|
700
|
+
*/
|
|
701
|
+
async revokeDelegation(cid) {
|
|
702
|
+
return this.delegationManager.revoke(cid);
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* List all delegations for the current session's space.
|
|
706
|
+
*
|
|
707
|
+
* @returns Result containing an array of Delegations
|
|
708
|
+
*/
|
|
709
|
+
async listDelegations() {
|
|
710
|
+
return this.delegationManager.list();
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Check if the current session has permission for a path and action.
|
|
714
|
+
*
|
|
715
|
+
* @param path - The resource path to check
|
|
716
|
+
* @param action - The action to check (e.g., "tinycloud.kv/get")
|
|
717
|
+
* @returns Result containing boolean permission status
|
|
718
|
+
*/
|
|
719
|
+
async checkPermission(path, action) {
|
|
720
|
+
return this.delegationManager.checkPermission(path, action);
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Create a delegation from this user to another user.
|
|
724
|
+
*
|
|
725
|
+
* The delegation grants the recipient access to a specific path and actions
|
|
726
|
+
* within this user's space.
|
|
727
|
+
*
|
|
728
|
+
* @param params - Delegation parameters
|
|
729
|
+
* @returns A portable delegation that can be sent to the recipient
|
|
730
|
+
*/
|
|
731
|
+
async createDelegation(params) {
|
|
732
|
+
if (!this.signer) {
|
|
733
|
+
throw new Error("Cannot createDelegation() in session-only mode. Requires wallet mode.");
|
|
734
|
+
}
|
|
735
|
+
const session = this.auth?.tinyCloudSession;
|
|
736
|
+
if (!session) {
|
|
737
|
+
throw new Error("Not signed in. Call signIn() first.");
|
|
738
|
+
}
|
|
739
|
+
// Build abilities for the delegation
|
|
740
|
+
const abilities = {
|
|
741
|
+
kv: {
|
|
742
|
+
[params.path]: params.actions,
|
|
743
|
+
},
|
|
744
|
+
};
|
|
745
|
+
const now = new Date();
|
|
746
|
+
const expiryMs = params.expiryMs ?? 60 * 60 * 1000; // Default 1 hour
|
|
747
|
+
const expirationTime = new Date(now.getTime() + expiryMs);
|
|
748
|
+
// Prepare the delegation session with:
|
|
749
|
+
// - delegateUri: target the recipient's DID directly (for user-to-user delegation)
|
|
750
|
+
// - parents: reference our session CID for chain validation
|
|
751
|
+
const prepared = prepareSession({
|
|
752
|
+
abilities,
|
|
753
|
+
address: ensureEip55(session.address),
|
|
754
|
+
chainId: session.chainId,
|
|
755
|
+
domain: new URL(this.config.host).hostname,
|
|
756
|
+
issuedAt: now.toISOString(),
|
|
757
|
+
expirationTime: expirationTime.toISOString(),
|
|
758
|
+
spaceId: session.spaceId,
|
|
759
|
+
delegateUri: params.delegateDID,
|
|
760
|
+
parents: [session.delegationCid],
|
|
761
|
+
});
|
|
762
|
+
// Sign the SIWE message with this user's signer
|
|
763
|
+
const signature = await this.signer.signMessage(prepared.siwe);
|
|
764
|
+
// Complete the session setup
|
|
765
|
+
const delegationSession = completeSessionSetup({
|
|
766
|
+
...prepared,
|
|
767
|
+
signature,
|
|
768
|
+
});
|
|
769
|
+
// Activate the delegation with the server
|
|
770
|
+
const activateResult = await activateSessionWithHost(this.config.host, delegationSession.delegationHeader);
|
|
771
|
+
if (!activateResult.success) {
|
|
772
|
+
throw new Error(`Failed to activate delegation: ${activateResult.error}`);
|
|
773
|
+
}
|
|
774
|
+
// Return the portable delegation
|
|
775
|
+
return {
|
|
776
|
+
cid: delegationSession.delegationCid,
|
|
777
|
+
delegationHeader: delegationSession.delegationHeader,
|
|
778
|
+
spaceId: session.spaceId,
|
|
779
|
+
path: params.path,
|
|
780
|
+
actions: params.actions,
|
|
781
|
+
disableSubDelegation: params.disableSubDelegation ?? false,
|
|
782
|
+
expiry: expirationTime,
|
|
783
|
+
delegateDID: params.delegateDID,
|
|
784
|
+
ownerAddress: session.address,
|
|
785
|
+
chainId: session.chainId,
|
|
786
|
+
host: this.config.host,
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Use a delegation received from another user.
|
|
791
|
+
*
|
|
792
|
+
* This creates a new session key for this user that chains from the
|
|
793
|
+
* received delegation, allowing operations on the delegator's space.
|
|
794
|
+
*
|
|
795
|
+
* Works in both modes:
|
|
796
|
+
* - **Wallet mode**: Creates a SIWE sub-delegation from PKH to session key
|
|
797
|
+
* - **Session-only mode**: Uses the delegation directly (must target session key DID)
|
|
798
|
+
*
|
|
799
|
+
* @param delegation - The PortableDelegation to use (from createDelegation or transport)
|
|
800
|
+
* @returns A DelegatedAccess instance for performing operations
|
|
801
|
+
*/
|
|
802
|
+
async useDelegation(delegation) {
|
|
803
|
+
const delegationHeader = delegation.delegationHeader;
|
|
804
|
+
// Use the host from the delegation if provided, otherwise fall back to config
|
|
805
|
+
const targetHost = delegation.host ?? this.config.host;
|
|
806
|
+
// Session-only mode: use the delegation directly
|
|
807
|
+
// The delegation must target this user's session key DID
|
|
808
|
+
if (this.isSessionOnly) {
|
|
809
|
+
// Verify the delegation targets our session key DID
|
|
810
|
+
const myDid = this.did; // In session-only mode, this is the session key DID
|
|
811
|
+
if (delegation.delegateDID !== myDid) {
|
|
812
|
+
throw new Error(`Delegation targets ${delegation.delegateDID} but this user's DID is ${myDid}. ` +
|
|
813
|
+
`The delegation must target this user's DID.`);
|
|
814
|
+
}
|
|
815
|
+
// Create a session using the delegation directly
|
|
816
|
+
// In session-only mode, we use the received delegation as-is
|
|
817
|
+
const session = {
|
|
818
|
+
address: delegation.ownerAddress,
|
|
819
|
+
chainId: delegation.chainId,
|
|
820
|
+
sessionKey: JSON.stringify(this.sessionKeyJwk),
|
|
821
|
+
spaceId: delegation.spaceId,
|
|
822
|
+
delegationCid: delegation.cid,
|
|
823
|
+
delegationHeader,
|
|
824
|
+
verificationMethod: this.sessionDid,
|
|
825
|
+
jwk: this.sessionKeyJwk,
|
|
826
|
+
siwe: "", // Not used in session-only mode
|
|
827
|
+
signature: "", // Not used in session-only mode
|
|
828
|
+
};
|
|
829
|
+
// Track received delegation in registry
|
|
830
|
+
this.trackReceivedDelegation(delegation, this.sessionKeyJwk);
|
|
831
|
+
return new DelegatedAccess(session, delegation, targetHost);
|
|
832
|
+
}
|
|
833
|
+
// Wallet mode: create a SIWE sub-delegation
|
|
834
|
+
const mySession = this.auth?.tinyCloudSession;
|
|
835
|
+
if (!mySession) {
|
|
836
|
+
throw new Error("Not signed in. Call signIn() first.");
|
|
837
|
+
}
|
|
838
|
+
// Use our existing session key - the delegation targets our DID from signIn
|
|
839
|
+
// We must use the same key that the delegation was created for
|
|
840
|
+
const jwk = mySession.jwk;
|
|
841
|
+
// Build abilities from the delegation
|
|
842
|
+
const abilities = {
|
|
843
|
+
kv: {
|
|
844
|
+
[delegation.path]: delegation.actions,
|
|
845
|
+
},
|
|
846
|
+
};
|
|
847
|
+
const now = new Date();
|
|
848
|
+
// Use delegation expiry or 1 hour, whichever is sooner
|
|
849
|
+
const maxExpiry = new Date(now.getTime() + 60 * 60 * 1000);
|
|
850
|
+
const expirationTime = delegation.expiry < maxExpiry ? delegation.expiry : maxExpiry;
|
|
851
|
+
// Prepare the session with:
|
|
852
|
+
// - THIS user's address (we are the invoker)
|
|
853
|
+
// - The delegation owner's space (where we're accessing data)
|
|
854
|
+
// - Our existing session key (must match the DID the delegation targets)
|
|
855
|
+
// - Parent reference to the received delegation
|
|
856
|
+
const prepared = prepareSession({
|
|
857
|
+
abilities,
|
|
858
|
+
address: ensureEip55(mySession.address),
|
|
859
|
+
chainId: mySession.chainId,
|
|
860
|
+
domain: new URL(targetHost).hostname,
|
|
861
|
+
issuedAt: now.toISOString(),
|
|
862
|
+
expirationTime: expirationTime.toISOString(),
|
|
863
|
+
spaceId: delegation.spaceId,
|
|
864
|
+
jwk,
|
|
865
|
+
parents: [delegation.cid],
|
|
866
|
+
});
|
|
867
|
+
// Sign with THIS user's signer
|
|
868
|
+
const signature = await this.signer.signMessage(prepared.siwe);
|
|
869
|
+
// Complete the session setup
|
|
870
|
+
const invokerSession = completeSessionSetup({
|
|
871
|
+
...prepared,
|
|
872
|
+
signature,
|
|
873
|
+
});
|
|
874
|
+
// Activate with server
|
|
875
|
+
const activateResult = await activateSessionWithHost(targetHost, invokerSession.delegationHeader);
|
|
876
|
+
if (!activateResult.success) {
|
|
877
|
+
throw new Error(`Failed to activate delegated session: ${activateResult.error}`);
|
|
878
|
+
}
|
|
879
|
+
// Create TinyCloudSession for the delegated access
|
|
880
|
+
const session = {
|
|
881
|
+
address: mySession.address,
|
|
882
|
+
chainId: mySession.chainId,
|
|
883
|
+
sessionKey: mySession.sessionKey,
|
|
884
|
+
spaceId: delegation.spaceId,
|
|
885
|
+
delegationCid: invokerSession.delegationCid,
|
|
886
|
+
delegationHeader: invokerSession.delegationHeader,
|
|
887
|
+
verificationMethod: mySession.verificationMethod,
|
|
888
|
+
jwk,
|
|
889
|
+
siwe: prepared.siwe,
|
|
890
|
+
signature,
|
|
891
|
+
};
|
|
892
|
+
// Track received delegation in registry
|
|
893
|
+
this.trackReceivedDelegation(delegation, jwk);
|
|
894
|
+
return new DelegatedAccess(session, delegation, targetHost);
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Create a sub-delegation from a received delegation.
|
|
898
|
+
*
|
|
899
|
+
* This allows further delegating access that was received from another user,
|
|
900
|
+
* if the original delegation allows sub-delegation.
|
|
901
|
+
*
|
|
902
|
+
* @param parentDelegation - The delegation received from another user
|
|
903
|
+
* @param params - Sub-delegation parameters (must be within parent's scope)
|
|
904
|
+
* @returns A portable delegation for the sub-delegate
|
|
905
|
+
*/
|
|
906
|
+
async createSubDelegation(parentDelegation, params) {
|
|
907
|
+
if (!this.signer) {
|
|
908
|
+
throw new Error("Cannot createSubDelegation() in session-only mode. Requires wallet mode.");
|
|
909
|
+
}
|
|
910
|
+
if (!this._address) {
|
|
911
|
+
throw new Error("Not signed in. Call signIn() first.");
|
|
912
|
+
}
|
|
913
|
+
// Validate sub-delegation is allowed
|
|
914
|
+
if (parentDelegation.disableSubDelegation) {
|
|
915
|
+
throw new Error("Parent delegation does not allow sub-delegation");
|
|
916
|
+
}
|
|
917
|
+
// Validate path is within parent's path
|
|
918
|
+
if (!params.path.startsWith(parentDelegation.path)) {
|
|
919
|
+
throw new Error(`Sub-delegation path "${params.path}" must be within parent path "${parentDelegation.path}"`);
|
|
920
|
+
}
|
|
921
|
+
// Validate actions are subset of parent's actions
|
|
922
|
+
const parentActions = new Set(parentDelegation.actions);
|
|
923
|
+
for (const action of params.actions) {
|
|
924
|
+
if (!parentActions.has(action)) {
|
|
925
|
+
throw new Error(`Sub-delegation action "${action}" is not in parent's actions: ${parentDelegation.actions.join(", ")}`);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
// Calculate expiry - cap at parent's expiry
|
|
929
|
+
const now = new Date();
|
|
930
|
+
const expiryMs = params.expiryMs ?? 60 * 60 * 1000;
|
|
931
|
+
const requestedExpiry = new Date(now.getTime() + expiryMs);
|
|
932
|
+
// Sub-delegation cannot outlive parent, so cap at parent's expiry
|
|
933
|
+
const actualExpiry = requestedExpiry > parentDelegation.expiry ? parentDelegation.expiry : requestedExpiry;
|
|
934
|
+
// Build abilities for the sub-delegation
|
|
935
|
+
const abilities = {
|
|
936
|
+
kv: {
|
|
937
|
+
[params.path]: params.actions,
|
|
938
|
+
},
|
|
939
|
+
};
|
|
940
|
+
// Use parent's host or fall back to config
|
|
941
|
+
const targetHost = parentDelegation.host ?? this.config.host;
|
|
942
|
+
// Prepare the sub-delegation session
|
|
943
|
+
// Uses THIS user's address (who received the delegation and is now sub-delegating)
|
|
944
|
+
// Targets the recipient's PKH DID (delegateUri)
|
|
945
|
+
// References the parent delegation as the chain
|
|
946
|
+
const prepared = prepareSession({
|
|
947
|
+
abilities,
|
|
948
|
+
address: ensureEip55(this._address),
|
|
949
|
+
chainId: this._chainId,
|
|
950
|
+
domain: new URL(targetHost).hostname,
|
|
951
|
+
issuedAt: now.toISOString(),
|
|
952
|
+
expirationTime: actualExpiry.toISOString(),
|
|
953
|
+
spaceId: parentDelegation.spaceId,
|
|
954
|
+
delegateUri: params.delegateDID,
|
|
955
|
+
parents: [parentDelegation.cid],
|
|
956
|
+
});
|
|
957
|
+
// Sign with THIS user's signer
|
|
958
|
+
const signature = await this.signer.signMessage(prepared.siwe);
|
|
959
|
+
// Complete the session setup
|
|
960
|
+
const subDelegationSession = completeSessionSetup({
|
|
961
|
+
...prepared,
|
|
962
|
+
signature,
|
|
963
|
+
});
|
|
964
|
+
// Activate the sub-delegation with the server
|
|
965
|
+
const activateResult = await activateSessionWithHost(targetHost, subDelegationSession.delegationHeader);
|
|
966
|
+
if (!activateResult.success) {
|
|
967
|
+
throw new Error(`Failed to activate sub-delegation: ${activateResult.error}`);
|
|
968
|
+
}
|
|
969
|
+
// Return the portable sub-delegation
|
|
970
|
+
return {
|
|
971
|
+
cid: subDelegationSession.delegationCid,
|
|
972
|
+
delegationHeader: subDelegationSession.delegationHeader,
|
|
973
|
+
spaceId: parentDelegation.spaceId,
|
|
974
|
+
path: params.path,
|
|
975
|
+
actions: params.actions,
|
|
976
|
+
disableSubDelegation: params.disableSubDelegation ?? false,
|
|
977
|
+
expiry: actualExpiry,
|
|
978
|
+
delegateDID: params.delegateDID,
|
|
979
|
+
ownerAddress: parentDelegation.ownerAddress,
|
|
980
|
+
chainId: parentDelegation.chainId,
|
|
981
|
+
host: targetHost,
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
/** Flag to ensure WASM panic hook is only initialized once */
|
|
986
|
+
TinyCloudNode.wasmInitialized = false;
|
|
987
|
+
//# sourceMappingURL=TinyCloudNode.js.map
|