@inixiative/signet 0.4.1 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -0
- package/package.json +2 -2
- package/src/client.ts +100 -34
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.3",
|
|
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.93",
|
|
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,
|
|
@@ -117,7 +126,8 @@ const errorDetail = async (response: Response) => {
|
|
|
117
126
|
try {
|
|
118
127
|
const body = JSON.parse(text) as { message?: unknown; error?: unknown };
|
|
119
128
|
const error = body.error as { message?: unknown } | string | undefined;
|
|
120
|
-
const
|
|
129
|
+
const errorMessage = typeof error === 'string' ? error : error?.message;
|
|
130
|
+
const message = typeof body.message === 'string' ? body.message : errorMessage;
|
|
121
131
|
if (typeof message === 'string') return message.slice(0, 500);
|
|
122
132
|
} catch {}
|
|
123
133
|
return text.replace(/\s+/g, ' ').slice(0, 500) || undefined;
|
|
@@ -205,21 +215,25 @@ export async function signetProof(
|
|
|
205
215
|
});
|
|
206
216
|
}
|
|
207
217
|
|
|
208
|
-
/**
|
|
209
|
-
|
|
218
|
+
/**
|
|
219
|
+
* A call signed by the caller's key with no access token: an Installation acting as itself, or a
|
|
220
|
+
* person acting through `headers` (their Kingdom API token).
|
|
221
|
+
*/
|
|
222
|
+
async function keyPost<T>(
|
|
210
223
|
url: string,
|
|
211
224
|
keyFile: string,
|
|
212
225
|
action: string,
|
|
213
226
|
body: unknown,
|
|
214
227
|
schema: z.ZodType<T>,
|
|
215
228
|
options: SignetRequestOptions = {},
|
|
229
|
+
headers: Record<string, string> = {},
|
|
216
230
|
): Promise<T> {
|
|
217
231
|
return schema.parse(
|
|
218
232
|
await signetPost(
|
|
219
233
|
url,
|
|
220
234
|
action,
|
|
221
235
|
body,
|
|
222
|
-
{ DPoP: await signetProof(url, action, keyFile, undefined, options) },
|
|
236
|
+
{ ...headers, DPoP: await signetProof(url, action, keyFile, undefined, options) },
|
|
223
237
|
undefined,
|
|
224
238
|
options,
|
|
225
239
|
),
|
|
@@ -234,7 +248,7 @@ export const registerInstallation = (
|
|
|
234
248
|
options: SignetRequestOptions = {},
|
|
235
249
|
) =>
|
|
236
250
|
readPrivateJson(keyFile).then((key) =>
|
|
237
|
-
|
|
251
|
+
keyPost(
|
|
238
252
|
url,
|
|
239
253
|
keyFile,
|
|
240
254
|
'registerInstallation',
|
|
@@ -251,7 +265,7 @@ export const requestRegistration = (
|
|
|
251
265
|
terms: RegistrationRequest,
|
|
252
266
|
options: SignetRequestOptions = {},
|
|
253
267
|
) =>
|
|
254
|
-
|
|
268
|
+
keyPost(
|
|
255
269
|
url,
|
|
256
270
|
keyFile,
|
|
257
271
|
'requestRegistration',
|
|
@@ -266,14 +280,7 @@ export const installationInquiries = (
|
|
|
266
280
|
keyFile: string,
|
|
267
281
|
options: SignetRequestOptions = {},
|
|
268
282
|
) =>
|
|
269
|
-
|
|
270
|
-
url,
|
|
271
|
-
keyFile,
|
|
272
|
-
'installationInquiries',
|
|
273
|
-
{},
|
|
274
|
-
installationInquiriesResponseSchema,
|
|
275
|
-
options,
|
|
276
|
-
);
|
|
283
|
+
keyPost(url, keyFile, 'installationInquiries', {}, installationInquiriesResponseSchema, options);
|
|
277
284
|
|
|
278
285
|
export const cancelInstallationInquiry = (
|
|
279
286
|
url: string,
|
|
@@ -281,7 +288,7 @@ export const cancelInstallationInquiry = (
|
|
|
281
288
|
inquiryId: string,
|
|
282
289
|
options: SignetRequestOptions = {},
|
|
283
290
|
) =>
|
|
284
|
-
|
|
291
|
+
keyPost(
|
|
285
292
|
url,
|
|
286
293
|
keyFile,
|
|
287
294
|
'cancelInstallationInquiry',
|
|
@@ -301,7 +308,7 @@ export async function collectSignet(
|
|
|
301
308
|
options: SignetRequestOptions = {},
|
|
302
309
|
) {
|
|
303
310
|
try {
|
|
304
|
-
return await
|
|
311
|
+
return await keyPost(
|
|
305
312
|
url,
|
|
306
313
|
keyFile,
|
|
307
314
|
'collectSignet',
|
|
@@ -315,21 +322,87 @@ export async function collectSignet(
|
|
|
315
322
|
}
|
|
316
323
|
}
|
|
317
324
|
|
|
318
|
-
/**
|
|
319
|
-
export
|
|
325
|
+
/** Poll `collectSignet` until the request is approved; throws once it is declined or expires. */
|
|
326
|
+
export async function awaitSignet(
|
|
320
327
|
url: string,
|
|
321
328
|
keyFile: string,
|
|
329
|
+
inquiryId: string,
|
|
330
|
+
{ intervalMs = 5000, signal }: { intervalMs?: number; signal?: AbortSignal } = {},
|
|
331
|
+
) {
|
|
332
|
+
for (;;) {
|
|
333
|
+
signal?.throwIfAborted();
|
|
334
|
+
const collected = await collectSignet(url, keyFile, inquiryId, { signal });
|
|
335
|
+
if (collected) return collected;
|
|
336
|
+
await new Promise<void>((resolve, reject) => {
|
|
337
|
+
const done = () => {
|
|
338
|
+
signal?.removeEventListener('abort', aborted);
|
|
339
|
+
resolve();
|
|
340
|
+
};
|
|
341
|
+
const timer = setTimeout(done, intervalMs);
|
|
342
|
+
const aborted = () => {
|
|
343
|
+
clearTimeout(timer);
|
|
344
|
+
reject(signal?.reason);
|
|
345
|
+
};
|
|
346
|
+
signal?.addEventListener('abort', aborted, { once: true });
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Ask for a Signet on an integration, to be presented with this key. Kingdom records a
|
|
353
|
+
* `signetRequest` Inquiry (approved at once when the caller manages the integration); pass the
|
|
354
|
+
* returned `requestId` to `collectSignet` or `awaitSignet`.
|
|
355
|
+
*/
|
|
356
|
+
export const requestSignet = async (
|
|
357
|
+
url: string,
|
|
358
|
+
keyFile: string,
|
|
359
|
+
apiToken: string,
|
|
360
|
+
request: z.input<typeof signetProposalSchema>,
|
|
322
361
|
options: SignetRequestOptions = {},
|
|
323
|
-
) =>
|
|
324
|
-
|
|
362
|
+
): Promise<SignetRequestResponse> =>
|
|
363
|
+
keyPost(
|
|
325
364
|
url,
|
|
326
365
|
keyFile,
|
|
327
|
-
'
|
|
328
|
-
{
|
|
329
|
-
|
|
366
|
+
'requestSignet',
|
|
367
|
+
{
|
|
368
|
+
...signetProposalSchema.parse(request),
|
|
369
|
+
publicKey: signetPublicKey(await readPrivateJson(keyFile)),
|
|
370
|
+
},
|
|
371
|
+
signetRequestResponseSchema,
|
|
330
372
|
options,
|
|
373
|
+
{ authorization: `Bearer ${apiToken}` },
|
|
331
374
|
);
|
|
332
375
|
|
|
376
|
+
/**
|
|
377
|
+
* Enroll this key on a Signet issued to the caller (an owner's `owner/grant`), or re-enroll after
|
|
378
|
+
* the enrollment or its idle window expired. `expectedRevision` is the Signet's current revision.
|
|
379
|
+
*/
|
|
380
|
+
export const enrollSignet = async (
|
|
381
|
+
url: string,
|
|
382
|
+
keyFile: string,
|
|
383
|
+
apiToken: string,
|
|
384
|
+
enrollment: { signetId: string; expectedRevision: number; name: string },
|
|
385
|
+
options: SignetRequestOptions = {},
|
|
386
|
+
): Promise<EnrolledSignet> => ({
|
|
387
|
+
...(await keyPost(
|
|
388
|
+
url,
|
|
389
|
+
keyFile,
|
|
390
|
+
'enrollSignet',
|
|
391
|
+
{ ...enrollment, publicKey: signetPublicKey(await readPrivateJson(keyFile)) },
|
|
392
|
+
enrolledCredentialsSchema,
|
|
393
|
+
options,
|
|
394
|
+
{ authorization: `Bearer ${apiToken}` },
|
|
395
|
+
)),
|
|
396
|
+
signetId: enrollment.signetId,
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
/** The Signets held by this installation's integrations. */
|
|
400
|
+
export const installationSignets = (
|
|
401
|
+
url: string,
|
|
402
|
+
keyFile: string,
|
|
403
|
+
options: SignetRequestOptions = {},
|
|
404
|
+
) => keyPost(url, keyFile, 'installationSignets', {}, installationSignetsResponseSchema, options);
|
|
405
|
+
|
|
333
406
|
/** Enroll this installation's key on a Signet one of its integrations holds. */
|
|
334
407
|
export const enrollInstallationSignet = (
|
|
335
408
|
url: string,
|
|
@@ -337,14 +410,7 @@ export const enrollInstallationSignet = (
|
|
|
337
410
|
signetId: string,
|
|
338
411
|
options: SignetRequestOptions = {},
|
|
339
412
|
) =>
|
|
340
|
-
|
|
341
|
-
url,
|
|
342
|
-
keyFile,
|
|
343
|
-
'enrollInstallationSignet',
|
|
344
|
-
{ signetId },
|
|
345
|
-
collectedSignetSchema,
|
|
346
|
-
options,
|
|
347
|
-
);
|
|
413
|
+
keyPost(url, keyFile, 'enrollInstallationSignet', { signetId }, collectedSignetSchema, options);
|
|
348
414
|
|
|
349
415
|
/** Give Kingdom the credential it uses to reach this installation's integration (Foundry's tunnel token). */
|
|
350
416
|
export const setInstallationCredential = (
|
|
@@ -354,7 +420,7 @@ export const setInstallationCredential = (
|
|
|
354
420
|
token: string,
|
|
355
421
|
options: SignetRequestOptions = {},
|
|
356
422
|
) =>
|
|
357
|
-
|
|
423
|
+
keyPost(
|
|
358
424
|
url,
|
|
359
425
|
keyFile,
|
|
360
426
|
'setInstallationCredential',
|
|
@@ -379,7 +445,7 @@ export async function rotateInstallationKey(
|
|
|
379
445
|
currentThumbprint,
|
|
380
446
|
options,
|
|
381
447
|
);
|
|
382
|
-
return
|
|
448
|
+
return keyPost(
|
|
383
449
|
url,
|
|
384
450
|
keyFile,
|
|
385
451
|
'rotateInstallationKey',
|
|
@@ -394,7 +460,7 @@ export async function saveCollectedSignet(
|
|
|
394
460
|
credentialFile: string,
|
|
395
461
|
url: string,
|
|
396
462
|
keyFile: string,
|
|
397
|
-
collected:
|
|
463
|
+
collected: DeliveredSignet,
|
|
398
464
|
) {
|
|
399
465
|
const credential = signetCredentialSchema.parse({ ...collected, url, keyFile });
|
|
400
466
|
await writePrivateJson(credentialFile, credential);
|