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