@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.
Files changed (3) hide show
  1. package/README.md +20 -0
  2. package/package.json +2 -2
  3. 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.1",
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.86",
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
- /** A call an Installation makes as itself: no access token, a DPoP proof by its key. */
209
- async function installationPost<T>(
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
- installationPost(
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
- installationPost(
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
- installationPost(
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
- installationPost(
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 installationPost(
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
- /** The Signets held by this installation's integrations. */
319
- export const installationSignets = (
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
- installationPost(
361
+ ): Promise<SignetRequestResponse> =>
362
+ keyPost(
325
363
  url,
326
364
  keyFile,
327
- 'installationSignets',
328
- {},
329
- installationSignetsResponseSchema,
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
- installationPost(
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
- installationPost(
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 installationPost(
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: z.infer<typeof collectedSignetSchema>,
462
+ collected: DeliveredSignet,
398
463
  ) {
399
464
  const credential = signetCredentialSchema.parse({ ...collected, url, keyFile });
400
465
  await writePrivateJson(credentialFile, credential);