@inixiative/signet 0.4.0 → 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 +106 -42
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,
|
|
@@ -111,17 +120,16 @@ function abortable<T>(pending: Promise<T>, signal: AbortSignal): Promise<T> {
|
|
|
111
120
|
});
|
|
112
121
|
}
|
|
113
122
|
|
|
123
|
+
/** Kingdom's JSON reason, else the start of the raw body (a proxy's page). */
|
|
114
124
|
const errorDetail = async (response: Response) => {
|
|
125
|
+
const text = (await response.text().catch(() => '')).slice(0, 65_536).trim();
|
|
115
126
|
try {
|
|
116
|
-
const body = JSON.parse(
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
} catch {
|
|
123
|
-
return undefined;
|
|
124
|
-
}
|
|
127
|
+
const body = JSON.parse(text) as { message?: unknown; error?: unknown };
|
|
128
|
+
const error = body.error as { message?: unknown } | string | undefined;
|
|
129
|
+
const message = typeof error === 'string' ? error : (error?.message ?? body.message);
|
|
130
|
+
if (typeof message === 'string') return message.slice(0, 500);
|
|
131
|
+
} catch {}
|
|
132
|
+
return text.replace(/\s+/g, ' ').slice(0, 500) || undefined;
|
|
125
133
|
};
|
|
126
134
|
|
|
127
135
|
/** POST to Kingdom's access API and return the response's `data`. */
|
|
@@ -206,21 +214,25 @@ export async function signetProof(
|
|
|
206
214
|
});
|
|
207
215
|
}
|
|
208
216
|
|
|
209
|
-
/**
|
|
210
|
-
|
|
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>(
|
|
211
222
|
url: string,
|
|
212
223
|
keyFile: string,
|
|
213
224
|
action: string,
|
|
214
225
|
body: unknown,
|
|
215
226
|
schema: z.ZodType<T>,
|
|
216
227
|
options: SignetRequestOptions = {},
|
|
228
|
+
headers: Record<string, string> = {},
|
|
217
229
|
): Promise<T> {
|
|
218
230
|
return schema.parse(
|
|
219
231
|
await signetPost(
|
|
220
232
|
url,
|
|
221
233
|
action,
|
|
222
234
|
body,
|
|
223
|
-
{ DPoP: await signetProof(url, action, keyFile, undefined, options) },
|
|
235
|
+
{ ...headers, DPoP: await signetProof(url, action, keyFile, undefined, options) },
|
|
224
236
|
undefined,
|
|
225
237
|
options,
|
|
226
238
|
),
|
|
@@ -235,7 +247,7 @@ export const registerInstallation = (
|
|
|
235
247
|
options: SignetRequestOptions = {},
|
|
236
248
|
) =>
|
|
237
249
|
readPrivateJson(keyFile).then((key) =>
|
|
238
|
-
|
|
250
|
+
keyPost(
|
|
239
251
|
url,
|
|
240
252
|
keyFile,
|
|
241
253
|
'registerInstallation',
|
|
@@ -252,7 +264,7 @@ export const requestRegistration = (
|
|
|
252
264
|
terms: RegistrationRequest,
|
|
253
265
|
options: SignetRequestOptions = {},
|
|
254
266
|
) =>
|
|
255
|
-
|
|
267
|
+
keyPost(
|
|
256
268
|
url,
|
|
257
269
|
keyFile,
|
|
258
270
|
'requestRegistration',
|
|
@@ -267,14 +279,7 @@ export const installationInquiries = (
|
|
|
267
279
|
keyFile: string,
|
|
268
280
|
options: SignetRequestOptions = {},
|
|
269
281
|
) =>
|
|
270
|
-
|
|
271
|
-
url,
|
|
272
|
-
keyFile,
|
|
273
|
-
'installationInquiries',
|
|
274
|
-
{},
|
|
275
|
-
installationInquiriesResponseSchema,
|
|
276
|
-
options,
|
|
277
|
-
);
|
|
282
|
+
keyPost(url, keyFile, 'installationInquiries', {}, installationInquiriesResponseSchema, options);
|
|
278
283
|
|
|
279
284
|
export const cancelInstallationInquiry = (
|
|
280
285
|
url: string,
|
|
@@ -282,7 +287,7 @@ export const cancelInstallationInquiry = (
|
|
|
282
287
|
inquiryId: string,
|
|
283
288
|
options: SignetRequestOptions = {},
|
|
284
289
|
) =>
|
|
285
|
-
|
|
290
|
+
keyPost(
|
|
286
291
|
url,
|
|
287
292
|
keyFile,
|
|
288
293
|
'cancelInstallationInquiry',
|
|
@@ -302,7 +307,7 @@ export async function collectSignet(
|
|
|
302
307
|
options: SignetRequestOptions = {},
|
|
303
308
|
) {
|
|
304
309
|
try {
|
|
305
|
-
return await
|
|
310
|
+
return await keyPost(
|
|
306
311
|
url,
|
|
307
312
|
keyFile,
|
|
308
313
|
'collectSignet',
|
|
@@ -316,21 +321,87 @@ export async function collectSignet(
|
|
|
316
321
|
}
|
|
317
322
|
}
|
|
318
323
|
|
|
319
|
-
/**
|
|
320
|
-
export
|
|
324
|
+
/** Poll `collectSignet` until the request is approved; throws once it is declined or expires. */
|
|
325
|
+
export async function awaitSignet(
|
|
321
326
|
url: string,
|
|
322
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>,
|
|
323
360
|
options: SignetRequestOptions = {},
|
|
324
|
-
) =>
|
|
325
|
-
|
|
361
|
+
): Promise<SignetRequestResponse> =>
|
|
362
|
+
keyPost(
|
|
326
363
|
url,
|
|
327
364
|
keyFile,
|
|
328
|
-
'
|
|
329
|
-
{
|
|
330
|
-
|
|
365
|
+
'requestSignet',
|
|
366
|
+
{
|
|
367
|
+
...signetProposalSchema.parse(request),
|
|
368
|
+
publicKey: signetPublicKey(await readPrivateJson(keyFile)),
|
|
369
|
+
},
|
|
370
|
+
signetRequestResponseSchema,
|
|
331
371
|
options,
|
|
372
|
+
{ authorization: `Bearer ${apiToken}` },
|
|
332
373
|
);
|
|
333
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
|
+
|
|
334
405
|
/** Enroll this installation's key on a Signet one of its integrations holds. */
|
|
335
406
|
export const enrollInstallationSignet = (
|
|
336
407
|
url: string,
|
|
@@ -338,14 +409,7 @@ export const enrollInstallationSignet = (
|
|
|
338
409
|
signetId: string,
|
|
339
410
|
options: SignetRequestOptions = {},
|
|
340
411
|
) =>
|
|
341
|
-
|
|
342
|
-
url,
|
|
343
|
-
keyFile,
|
|
344
|
-
'enrollInstallationSignet',
|
|
345
|
-
{ signetId },
|
|
346
|
-
collectedSignetSchema,
|
|
347
|
-
options,
|
|
348
|
-
);
|
|
412
|
+
keyPost(url, keyFile, 'enrollInstallationSignet', { signetId }, collectedSignetSchema, options);
|
|
349
413
|
|
|
350
414
|
/** Give Kingdom the credential it uses to reach this installation's integration (Foundry's tunnel token). */
|
|
351
415
|
export const setInstallationCredential = (
|
|
@@ -355,7 +419,7 @@ export const setInstallationCredential = (
|
|
|
355
419
|
token: string,
|
|
356
420
|
options: SignetRequestOptions = {},
|
|
357
421
|
) =>
|
|
358
|
-
|
|
422
|
+
keyPost(
|
|
359
423
|
url,
|
|
360
424
|
keyFile,
|
|
361
425
|
'setInstallationCredential',
|
|
@@ -380,7 +444,7 @@ export async function rotateInstallationKey(
|
|
|
380
444
|
currentThumbprint,
|
|
381
445
|
options,
|
|
382
446
|
);
|
|
383
|
-
return
|
|
447
|
+
return keyPost(
|
|
384
448
|
url,
|
|
385
449
|
keyFile,
|
|
386
450
|
'rotateInstallationKey',
|
|
@@ -395,7 +459,7 @@ export async function saveCollectedSignet(
|
|
|
395
459
|
credentialFile: string,
|
|
396
460
|
url: string,
|
|
397
461
|
keyFile: string,
|
|
398
|
-
collected:
|
|
462
|
+
collected: DeliveredSignet,
|
|
399
463
|
) {
|
|
400
464
|
const credential = signetCredentialSchema.parse({ ...collected, url, keyFile });
|
|
401
465
|
await writePrivateJson(credentialFile, credential);
|