@inixiative/signet 0.4.1 → 0.4.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.
- package/README.md +20 -0
- package/package.json +2 -2
- package/src/client.ts +98 -33
package/README.md
CHANGED
|
@@ -13,6 +13,7 @@ This package is the one implementation of that protocol, shared by Kingdom (whic
|
|
|
13
13
|
- an **Installation** (a local Foundry, Archive or Oracle) is an actor known to Kingdom by its key: `registerInstallation`, then `requestRegistration` asks for what it needs (the owner it expects, by organization id or slug, and resources by id or by kind and name, with their operations) and returns a review code a person claims in Kingdom (a `registerIntegration` Inquiry), where the review opens with that owner and those resources selected, then `collectSignet` and `saveCollectedSignet`. `installationInquiries`, `installationSignets`, `cancelInstallationInquiry`, `enrollInstallationSignet` and `rotateInstallationKey` act as the installation;
|
|
14
14
|
- `pairInstallation` does the whole pairing (register, request, wait on the socket, confirm the owner, collect) and `holdInstallationSignets` keeps one credential file per Signet the installation's integrations hold, enrolling later grants and dropping revoked ones;
|
|
15
15
|
- `InstallationSocket`, Kingdom's live line to an installation: it proves the key, receives a snapshot of the installation's inquiries and Signets on connect and on every change, keeps the installation's integrations marked online, advertises its viewer URL, and polls the same reads while the socket is down;
|
|
16
|
+
- a **person or their agent** acts as themselves with a Kingdom API token plus a proof from its key: `requestSignet` asks for a Signet on an integration (a `signetRequest` Inquiry, approved at once when the caller manages the integration), `awaitSignet` polls `collectSignet` until it is approved, and `enrollSignet` enrolls the key on a Signet an owner granted directly (`owner/grant`). `saveCollectedSignet` writes either result next to the key;
|
|
16
17
|
- `SignetClient`, which renews its access token and signs each call: `describe`, `execute`, task settlement, and the run actions a Foundry uses for inference (`resolveRun`, `delegateRun`, `renewRun`, `revokeRun`), and for a subscription run `releaseSubscription` and `reportSubscriptionLimits`.
|
|
17
18
|
|
|
18
19
|
```ts
|
|
@@ -39,4 +40,23 @@ const signet = await SignetClient.fromFile(paired.credentialFile);
|
|
|
39
40
|
await signet.execute({ integrationId, operation: 'sessions.write', input: { resourceId, snapshot, previousDigest: null } });
|
|
40
41
|
```
|
|
41
42
|
|
|
43
|
+
A person's agent, with an API token minted in Kingdom:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { awaitSignet, enrollSignet, generateSignetKey, requestSignet, saveCollectedSignet, SignetClient, writePrivateJson } from '@inixiative/signet';
|
|
47
|
+
|
|
48
|
+
await writePrivateJson(keyFile, generateSignetKey());
|
|
49
|
+
const { requestId, reviewCode } = await requestSignet(kingdom, keyFile, apiToken, {
|
|
50
|
+
name: 'Research agent', integrationId, lifecycle: 'ongoing', expiresAt: null, maxRequests: null, maxConcurrent: 1,
|
|
51
|
+
resources: [{ resourceId, operations: ['documents.read'] }],
|
|
52
|
+
});
|
|
53
|
+
const collected = await awaitSignet(kingdom, keyFile, requestId); // or, for a granted Signet:
|
|
54
|
+
// const collected = await enrollSignet(kingdom, keyFile, apiToken, { signetId, expectedRevision, name: 'Research agent' });
|
|
55
|
+
await saveCollectedSignet(credentialFile, kingdom, keyFile, collected);
|
|
56
|
+
const signet = await SignetClient.fromFile(credentialFile);
|
|
57
|
+
await signet.describe();
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Kingdom's OpenAPI spec (`/openapi/docs`) publishes each step's raw HTTP call and credentials.
|
|
61
|
+
|
|
42
62
|
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inixiative/signet",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Present Kingdom Signets: the wire contracts, DPoP proofs, device pairing and a renewing client shared by Kingdom, Archive, Foundry and Oracle.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@biomejs/biome": "2.5.2",
|
|
37
|
-
"@inixiative/config": "^0.5.
|
|
37
|
+
"@inixiative/config": "^0.5.90",
|
|
38
38
|
"@types/bun": "1.4.2",
|
|
39
39
|
"lefthook": "2.1.9",
|
|
40
40
|
"typescript": "6.0.3",
|
package/src/client.ts
CHANGED
|
@@ -16,10 +16,14 @@ import {
|
|
|
16
16
|
registerInstallationResponseSchema,
|
|
17
17
|
renewalCredentialPattern,
|
|
18
18
|
requestRegistrationResponseSchema,
|
|
19
|
+
type SignetRequestResponse,
|
|
19
20
|
setInstallationCredentialResponseSchema,
|
|
21
|
+
signetCredentialsSchema,
|
|
20
22
|
signetDescriptionSchema,
|
|
21
23
|
signetKeySchema,
|
|
22
24
|
signetLifecycleSchema,
|
|
25
|
+
signetProposalSchema,
|
|
26
|
+
signetRequestResponseSchema,
|
|
23
27
|
} from './contracts';
|
|
24
28
|
import { readPrivateJson, writePrivateJson } from './files';
|
|
25
29
|
import { createSignetProof, keyThumbprint } from './proof';
|
|
@@ -60,7 +64,12 @@ export const signetCredentialSchema = z
|
|
|
60
64
|
})
|
|
61
65
|
.strict();
|
|
62
66
|
export type SignetCredential = z.infer<typeof signetCredentialSchema>;
|
|
67
|
+
/** A collected or enrolled Signet, as `saveCollectedSignet` writes it next to its key. */
|
|
63
68
|
export const deliveredSignetSchema = signetCredentialSchema.omit({ url: true, keyFile: true });
|
|
69
|
+
export type DeliveredSignet = z.infer<typeof deliveredSignetSchema>;
|
|
70
|
+
const enrolledCredentialsSchema = signetCredentialsSchema.required({ renewalCredential: true });
|
|
71
|
+
/** What `enrollSignet` returns: the enrollment's credentials and the Signet they present. */
|
|
72
|
+
export type EnrolledSignet = z.infer<typeof enrolledCredentialsSchema> & { signetId: string };
|
|
64
73
|
const renewalResponseSchema = deliveredSignetSchema.omit({
|
|
65
74
|
signetId: true,
|
|
66
75
|
integrationId: true,
|
|
@@ -205,21 +214,25 @@ export async function signetProof(
|
|
|
205
214
|
});
|
|
206
215
|
}
|
|
207
216
|
|
|
208
|
-
/**
|
|
209
|
-
|
|
217
|
+
/**
|
|
218
|
+
* A call signed by the caller's key with no access token: an Installation acting as itself, or a
|
|
219
|
+
* person acting through `headers` (their Kingdom API token).
|
|
220
|
+
*/
|
|
221
|
+
async function keyPost<T>(
|
|
210
222
|
url: string,
|
|
211
223
|
keyFile: string,
|
|
212
224
|
action: string,
|
|
213
225
|
body: unknown,
|
|
214
226
|
schema: z.ZodType<T>,
|
|
215
227
|
options: SignetRequestOptions = {},
|
|
228
|
+
headers: Record<string, string> = {},
|
|
216
229
|
): Promise<T> {
|
|
217
230
|
return schema.parse(
|
|
218
231
|
await signetPost(
|
|
219
232
|
url,
|
|
220
233
|
action,
|
|
221
234
|
body,
|
|
222
|
-
{ DPoP: await signetProof(url, action, keyFile, undefined, options) },
|
|
235
|
+
{ ...headers, DPoP: await signetProof(url, action, keyFile, undefined, options) },
|
|
223
236
|
undefined,
|
|
224
237
|
options,
|
|
225
238
|
),
|
|
@@ -234,7 +247,7 @@ export const registerInstallation = (
|
|
|
234
247
|
options: SignetRequestOptions = {},
|
|
235
248
|
) =>
|
|
236
249
|
readPrivateJson(keyFile).then((key) =>
|
|
237
|
-
|
|
250
|
+
keyPost(
|
|
238
251
|
url,
|
|
239
252
|
keyFile,
|
|
240
253
|
'registerInstallation',
|
|
@@ -251,7 +264,7 @@ export const requestRegistration = (
|
|
|
251
264
|
terms: RegistrationRequest,
|
|
252
265
|
options: SignetRequestOptions = {},
|
|
253
266
|
) =>
|
|
254
|
-
|
|
267
|
+
keyPost(
|
|
255
268
|
url,
|
|
256
269
|
keyFile,
|
|
257
270
|
'requestRegistration',
|
|
@@ -266,14 +279,7 @@ export const installationInquiries = (
|
|
|
266
279
|
keyFile: string,
|
|
267
280
|
options: SignetRequestOptions = {},
|
|
268
281
|
) =>
|
|
269
|
-
|
|
270
|
-
url,
|
|
271
|
-
keyFile,
|
|
272
|
-
'installationInquiries',
|
|
273
|
-
{},
|
|
274
|
-
installationInquiriesResponseSchema,
|
|
275
|
-
options,
|
|
276
|
-
);
|
|
282
|
+
keyPost(url, keyFile, 'installationInquiries', {}, installationInquiriesResponseSchema, options);
|
|
277
283
|
|
|
278
284
|
export const cancelInstallationInquiry = (
|
|
279
285
|
url: string,
|
|
@@ -281,7 +287,7 @@ export const cancelInstallationInquiry = (
|
|
|
281
287
|
inquiryId: string,
|
|
282
288
|
options: SignetRequestOptions = {},
|
|
283
289
|
) =>
|
|
284
|
-
|
|
290
|
+
keyPost(
|
|
285
291
|
url,
|
|
286
292
|
keyFile,
|
|
287
293
|
'cancelInstallationInquiry',
|
|
@@ -301,7 +307,7 @@ export async function collectSignet(
|
|
|
301
307
|
options: SignetRequestOptions = {},
|
|
302
308
|
) {
|
|
303
309
|
try {
|
|
304
|
-
return await
|
|
310
|
+
return await keyPost(
|
|
305
311
|
url,
|
|
306
312
|
keyFile,
|
|
307
313
|
'collectSignet',
|
|
@@ -315,21 +321,87 @@ export async function collectSignet(
|
|
|
315
321
|
}
|
|
316
322
|
}
|
|
317
323
|
|
|
318
|
-
/**
|
|
319
|
-
export
|
|
324
|
+
/** Poll `collectSignet` until the request is approved; throws once it is declined or expires. */
|
|
325
|
+
export async function awaitSignet(
|
|
320
326
|
url: string,
|
|
321
327
|
keyFile: string,
|
|
328
|
+
inquiryId: string,
|
|
329
|
+
{ intervalMs = 5000, signal }: { intervalMs?: number; signal?: AbortSignal } = {},
|
|
330
|
+
) {
|
|
331
|
+
for (;;) {
|
|
332
|
+
signal?.throwIfAborted();
|
|
333
|
+
const collected = await collectSignet(url, keyFile, inquiryId, { signal });
|
|
334
|
+
if (collected) return collected;
|
|
335
|
+
await new Promise<void>((resolve, reject) => {
|
|
336
|
+
const done = () => {
|
|
337
|
+
signal?.removeEventListener('abort', aborted);
|
|
338
|
+
resolve();
|
|
339
|
+
};
|
|
340
|
+
const timer = setTimeout(done, intervalMs);
|
|
341
|
+
const aborted = () => {
|
|
342
|
+
clearTimeout(timer);
|
|
343
|
+
reject(signal?.reason);
|
|
344
|
+
};
|
|
345
|
+
signal?.addEventListener('abort', aborted, { once: true });
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Ask for a Signet on an integration, to be presented with this key. Kingdom records a
|
|
352
|
+
* `signetRequest` Inquiry (approved at once when the caller manages the integration); pass the
|
|
353
|
+
* returned `requestId` to `collectSignet` or `awaitSignet`.
|
|
354
|
+
*/
|
|
355
|
+
export const requestSignet = async (
|
|
356
|
+
url: string,
|
|
357
|
+
keyFile: string,
|
|
358
|
+
apiToken: string,
|
|
359
|
+
request: z.input<typeof signetProposalSchema>,
|
|
322
360
|
options: SignetRequestOptions = {},
|
|
323
|
-
) =>
|
|
324
|
-
|
|
361
|
+
): Promise<SignetRequestResponse> =>
|
|
362
|
+
keyPost(
|
|
325
363
|
url,
|
|
326
364
|
keyFile,
|
|
327
|
-
'
|
|
328
|
-
{
|
|
329
|
-
|
|
365
|
+
'requestSignet',
|
|
366
|
+
{
|
|
367
|
+
...signetProposalSchema.parse(request),
|
|
368
|
+
publicKey: signetPublicKey(await readPrivateJson(keyFile)),
|
|
369
|
+
},
|
|
370
|
+
signetRequestResponseSchema,
|
|
330
371
|
options,
|
|
372
|
+
{ authorization: `Bearer ${apiToken}` },
|
|
331
373
|
);
|
|
332
374
|
|
|
375
|
+
/**
|
|
376
|
+
* Enroll this key on a Signet issued to the caller (an owner's `owner/grant`), or re-enroll after
|
|
377
|
+
* the enrollment or its idle window expired. `expectedRevision` is the Signet's current revision.
|
|
378
|
+
*/
|
|
379
|
+
export const enrollSignet = async (
|
|
380
|
+
url: string,
|
|
381
|
+
keyFile: string,
|
|
382
|
+
apiToken: string,
|
|
383
|
+
enrollment: { signetId: string; expectedRevision: number; name: string },
|
|
384
|
+
options: SignetRequestOptions = {},
|
|
385
|
+
): Promise<EnrolledSignet> => ({
|
|
386
|
+
...(await keyPost(
|
|
387
|
+
url,
|
|
388
|
+
keyFile,
|
|
389
|
+
'enrollSignet',
|
|
390
|
+
{ ...enrollment, publicKey: signetPublicKey(await readPrivateJson(keyFile)) },
|
|
391
|
+
enrolledCredentialsSchema,
|
|
392
|
+
options,
|
|
393
|
+
{ authorization: `Bearer ${apiToken}` },
|
|
394
|
+
)),
|
|
395
|
+
signetId: enrollment.signetId,
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
/** The Signets held by this installation's integrations. */
|
|
399
|
+
export const installationSignets = (
|
|
400
|
+
url: string,
|
|
401
|
+
keyFile: string,
|
|
402
|
+
options: SignetRequestOptions = {},
|
|
403
|
+
) => keyPost(url, keyFile, 'installationSignets', {}, installationSignetsResponseSchema, options);
|
|
404
|
+
|
|
333
405
|
/** Enroll this installation's key on a Signet one of its integrations holds. */
|
|
334
406
|
export const enrollInstallationSignet = (
|
|
335
407
|
url: string,
|
|
@@ -337,14 +409,7 @@ export const enrollInstallationSignet = (
|
|
|
337
409
|
signetId: string,
|
|
338
410
|
options: SignetRequestOptions = {},
|
|
339
411
|
) =>
|
|
340
|
-
|
|
341
|
-
url,
|
|
342
|
-
keyFile,
|
|
343
|
-
'enrollInstallationSignet',
|
|
344
|
-
{ signetId },
|
|
345
|
-
collectedSignetSchema,
|
|
346
|
-
options,
|
|
347
|
-
);
|
|
412
|
+
keyPost(url, keyFile, 'enrollInstallationSignet', { signetId }, collectedSignetSchema, options);
|
|
348
413
|
|
|
349
414
|
/** Give Kingdom the credential it uses to reach this installation's integration (Foundry's tunnel token). */
|
|
350
415
|
export const setInstallationCredential = (
|
|
@@ -354,7 +419,7 @@ export const setInstallationCredential = (
|
|
|
354
419
|
token: string,
|
|
355
420
|
options: SignetRequestOptions = {},
|
|
356
421
|
) =>
|
|
357
|
-
|
|
422
|
+
keyPost(
|
|
358
423
|
url,
|
|
359
424
|
keyFile,
|
|
360
425
|
'setInstallationCredential',
|
|
@@ -379,7 +444,7 @@ export async function rotateInstallationKey(
|
|
|
379
444
|
currentThumbprint,
|
|
380
445
|
options,
|
|
381
446
|
);
|
|
382
|
-
return
|
|
447
|
+
return keyPost(
|
|
383
448
|
url,
|
|
384
449
|
keyFile,
|
|
385
450
|
'rotateInstallationKey',
|
|
@@ -394,7 +459,7 @@ export async function saveCollectedSignet(
|
|
|
394
459
|
credentialFile: string,
|
|
395
460
|
url: string,
|
|
396
461
|
keyFile: string,
|
|
397
|
-
collected:
|
|
462
|
+
collected: DeliveredSignet,
|
|
398
463
|
) {
|
|
399
464
|
const credential = signetCredentialSchema.parse({ ...collected, url, keyFile });
|
|
400
465
|
await writePrivateJson(credentialFile, credential);
|