@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.
Files changed (3) hide show
  1. package/README.md +20 -0
  2. package/package.json +2 -2
  3. 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.0",
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.78",
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((await response.text()).slice(0, 4096)) as {
117
- message?: unknown;
118
- error?: { message?: unknown };
119
- };
120
- const message = body.error?.message ?? body.message;
121
- return typeof message === 'string' ? message.slice(0, 500) : undefined;
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
- /** A call an Installation makes as itself: no access token, a DPoP proof by its key. */
210
- 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>(
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
- installationPost(
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
- installationPost(
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
- installationPost(
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
- installationPost(
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 installationPost(
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
- /** The Signets held by this installation's integrations. */
320
- export const installationSignets = (
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
- installationPost(
361
+ ): Promise<SignetRequestResponse> =>
362
+ keyPost(
326
363
  url,
327
364
  keyFile,
328
- 'installationSignets',
329
- {},
330
- installationSignetsResponseSchema,
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
- installationPost(
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
- installationPost(
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 installationPost(
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: z.infer<typeof collectedSignetSchema>,
462
+ collected: DeliveredSignet,
399
463
  ) {
400
464
  const credential = signetCredentialSchema.parse({ ...collected, url, keyFile });
401
465
  await writePrivateJson(credentialFile, credential);