@inixiative/signet 0.4.3 → 0.5.0

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 CHANGED
@@ -6,7 +6,7 @@ A **Signet** is Kingdom's only grant for using an integration. It names the reso
6
6
 
7
7
  This package is the one implementation of that protocol, shared by Kingdom (which verifies) and Archive, Foundry and Oracle (which present):
8
8
 
9
- - `contracts`: the wire schemas: keys, lenses, proposals, pairing requests, delivered credentials, `describe` and `execute`.
9
+ - `contracts`: the wire schemas: keys, lenses, proposals, pairing requests, delivered credentials, `describe` and `execute`, and the conversation frames.
10
10
  - `proof`: create and verify DPoP proofs (ES256, nonce-bound, access-token-bound).
11
11
  - `files`: private 0600 JSON files for keys and credentials.
12
12
  - `client`:
@@ -57,6 +57,35 @@ const signet = await SignetClient.fromFile(credentialFile);
57
57
  await signet.describe();
58
58
  ```
59
59
 
60
+ ## Agent conversations
61
+
62
+ A conversation wraps a host Foundry's main thread (Kingdom `docs/AGENT-CONVERSATIONS.md`, Foundry `docs/reference/CONVERSATIONS.md`). A member is a Signet on the host Foundry's one `threads` resource; Kingdom admits each call and relays it over the host's Installation socket, and stores no conversation or message.
63
+
64
+ - **Lens.** `signetLensSchema` is a union of strict shapes, `archiveLensSchema | threadsLensSchema`, because a resource grant names its resource by id with no kind. `resourceLensSchema(kind)` gives the shape a kind takes; Kingdom checks every grant with it against the kind it stores. A threads lens is `{ threads, view: 'own' | 'thread', delivery: 'note' | 'turn', outbound: 'review' | 'auto' }`, the terms defaulting to the narrowest value. `threads` is a selector, a union on `form`: v1 builds `{ form: 'ids', ids }` (1–100 thread ids); `withMember` and `tag` are named for standing Signets and fail to parse everywhere until a release adds them, so a new form never widens access silently. `narrowsSignetLens` is the rule for approving someone else's request: a subset of ids, `own` < `thread`, `note` < `turn`, `review` < `auto`.
65
+ - **Join requests** may name a resource by kind: `requestSignet` takes `{ resourceKind: 'threads', operations, lens }`, and Kingdom resolves it before review.
66
+ - **Per-entry reads.** `installationInquiries`, `installationSignets`, the socket snapshot and `describe` read each entry on its own. An entry this package can't read is dropped and listed in `dropped`, and the rest is kept. Inside a Signet, resources are read one by one: a resource with a lens this package can't read (a selector form from a newer Kingdom, a lens that doesn't fit its resource's kind) is dropped, and the Signet stays, so an installation never loses a held Signet, or mistakes a member for revoked, over one resource. The snapshot carries `hostedSignets` (members of this Foundry's conversations) and `hostedSignetRequests` (join requests aimed at its integrations); held Signets carry their `resources`.
67
+ - **Host.** `InstallationSocket` takes `threads`, one handler per operation (`thread.read`, `thread.prompt`, `thread.floor`, `prompt.resolve`, `thread.subscribe`), answering `threadResult(...)` or `threadRefused(code)`. A frame with an unknown selector is refused `unknown_selector` before any handler runs. `thread.subscribe` answers with the member's view and the `journalSeq` it was taken at, and the subscription starts there: `appendThread(subscriptionId, entry)` holds entries pushed meanwhile and never sends one twice. Each append carries a dense per-subscription `seq` (a filtered view skips journal entries, so `journalSeq` can't reveal loss); an entry that can't be sent still spends its `seq`, so Kingdom sees the gap and the member resyncs. A subscription ends on `thread.unsubscribe`, when Kingdom stops re-asserting its lease (`thread.lease`; dropped 90 s after the last one, so one late 45 s sweep is survived), or when the socket drops.
68
+ - **Member.** An outside agent holds its live views with `SignetSocket`: it authenticates as its Signet's presentation (`authenticateSignet`, a proof bound to `POST /api/v1/access/signetSocket` with `ath`), renews it before the five-minute token expires (`renewPresentation`), and opens one `accessThread` stream per thread: a snapshot, then appends, with Kingdom's `seq`/`resync` rules. Frames are handled in order, one open is in flight per stream, and Kingdom's close carries the reason: after a revision the stream reopens under the new lens, while the host is offline it retries with backoff, when the presentation lapsed the socket re-presents and reopens, and on revocation or expiry it ends (`onClosed`). Posts are separate `execute` calls (`thread.prompt`), each admitted and metered. A Foundry holding a conversation Signet opens the same streams over its Installation socket (`threadStreams`, `openThread`).
69
+
70
+ ```ts
71
+ import { SignetClient, SignetSocket } from '@inixiative/signet';
72
+
73
+ const signet = await SignetClient.fromFile(credentialFile);
74
+ const socket = new SignetSocket({
75
+ client: signet,
76
+ onSnapshot: ({ threadId }, view) => show(threadId, view.entries),
77
+ onEntry: ({ threadId }, entry) => append(threadId, entry),
78
+ onClosed: ({ threadId }, reason) => console.log(`left ${threadId}: ${reason}`),
79
+ });
80
+ socket.open(threadId);
81
+ socket.start();
82
+ await signet.execute({
83
+ integrationId,
84
+ operation: 'thread.prompt',
85
+ input: { resourceId, threadId, postId: crypto.randomUUID(), text: 'The role you wanted opened' },
86
+ });
87
+ ```
88
+
60
89
  Kingdom's OpenAPI spec (`/openapi/docs`) publishes each step's raw HTTP call and credentials.
61
90
 
62
91
  MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inixiative/signet",
3
- "version": "0.4.3",
3
+ "version": "0.5.0",
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.93",
37
+ "@inixiative/config": "^0.5.115",
38
38
  "@types/bun": "1.4.2",
39
39
  "lefthook": "2.1.9",
40
40
  "typescript": "6.0.3",
package/src/client.ts CHANGED
@@ -7,10 +7,11 @@ import {
7
7
  accessTokenPattern,
8
8
  collectedSignetSchema,
9
9
  type InstallationKind,
10
- installationInquiriesResponseSchema,
11
- installationSignetsResponseSchema,
12
10
  nonceSchema,
13
11
  ownerRefSchema,
12
+ parseInstallationInquiries,
13
+ parseInstallationSignets,
14
+ parseSignetDescription,
14
15
  publicClientKeySchema,
15
16
  type RegistrationRequest,
16
17
  registerInstallationResponseSchema,
@@ -19,11 +20,11 @@ import {
19
20
  type SignetRequestResponse,
20
21
  setInstallationCredentialResponseSchema,
21
22
  signetCredentialsSchema,
22
- signetDescriptionSchema,
23
23
  signetKeySchema,
24
24
  signetLifecycleSchema,
25
25
  signetProposalSchema,
26
26
  signetRequestResponseSchema,
27
+ signetSocketAction,
27
28
  } from './contracts';
28
29
  import { readPrivateJson, writePrivateJson } from './files';
29
30
  import { createSignetProof, keyThumbprint } from './proof';
@@ -224,7 +225,7 @@ async function keyPost<T>(
224
225
  keyFile: string,
225
226
  action: string,
226
227
  body: unknown,
227
- schema: z.ZodType<T>,
228
+ schema: z.ZodType<T, unknown>,
228
229
  options: SignetRequestOptions = {},
229
230
  headers: Record<string, string> = {},
230
231
  ): Promise<T> {
@@ -274,13 +275,23 @@ export const requestRegistration = (
274
275
  options,
275
276
  );
276
277
 
277
- /** This installation's pending request and its inquiries. */
278
+ /**
279
+ * This installation's pending request, its inquiries, and the join requests aimed at its
280
+ * integrations. Read per entry: an entry this package can't read is dropped and listed in `dropped`.
281
+ */
278
282
  export const installationInquiries = (
279
283
  url: string,
280
284
  keyFile: string,
281
285
  options: SignetRequestOptions = {},
282
286
  ) =>
283
- keyPost(url, keyFile, 'installationInquiries', {}, installationInquiriesResponseSchema, options);
287
+ keyPost(
288
+ url,
289
+ keyFile,
290
+ 'installationInquiries',
291
+ {},
292
+ z.unknown().transform((value) => parseInstallationInquiries(value)),
293
+ options,
294
+ );
284
295
 
285
296
  export const cancelInstallationInquiry = (
286
297
  url: string,
@@ -396,12 +407,23 @@ export const enrollSignet = async (
396
407
  signetId: enrollment.signetId,
397
408
  });
398
409
 
399
- /** The Signets held by this installation's integrations. */
410
+ /**
411
+ * The Signets held by this installation's integrations, and those issued on them (`hostedSignets`).
412
+ * Read per entry: a Signet whose lens this package can't read is dropped and listed in `dropped`.
413
+ */
400
414
  export const installationSignets = (
401
415
  url: string,
402
416
  keyFile: string,
403
417
  options: SignetRequestOptions = {},
404
- ) => keyPost(url, keyFile, 'installationSignets', {}, installationSignetsResponseSchema, options);
418
+ ) =>
419
+ keyPost(
420
+ url,
421
+ keyFile,
422
+ 'installationSignets',
423
+ {},
424
+ z.unknown().transform((value) => parseInstallationSignets(value)),
425
+ options,
426
+ );
405
427
 
406
428
  /** Enroll this installation's key on a Signet one of its integrations holds. */
407
429
  export const enrollInstallationSignet = (
@@ -498,7 +520,7 @@ export class SignetClient {
498
520
  constructor(
499
521
  readonly url: string,
500
522
  private credentialFile: string,
501
- private signetId: string,
523
+ readonly signetId: string,
502
524
  ) {
503
525
  this.url = kingdomUrl(url);
504
526
  }
@@ -588,11 +610,29 @@ export class SignetClient {
588
610
  { signal, timeoutMs: options.timeoutMs },
589
611
  );
590
612
  }
613
+ /** Read per entry: a resource whose lens this package can't read is dropped, listed in `dropped`. */
591
614
  async describe(options: SignetRequestOptions = {}) {
592
- return signetDescriptionSchema.parse(
615
+ return parseSignetDescription(
593
616
  await this.post('describe', { signetId: this.signetId }, undefined, options),
594
617
  );
595
618
  }
619
+ /**
620
+ * The access token and a proof bound to the Signet socket, for `authenticateSignet` (or, with
621
+ * `renew`, a freshly renewed token for `renewPresentation`). Each proof spends one Kingdom nonce.
622
+ */
623
+ async socketPresentation(options: SignetRequestOptions & { renew?: boolean } = {}) {
624
+ const signal = requestSignal(options);
625
+ const credential = await this.credentials(options.renew ?? false, signal);
626
+ signal.throwIfAborted();
627
+ const proof = await signetProof(
628
+ this.url,
629
+ signetSocketAction,
630
+ credential.keyFile,
631
+ credential.accessToken,
632
+ { signal, timeoutMs: options.timeoutMs },
633
+ );
634
+ return { accessToken: credential.accessToken, proof, expiresAt: credential.expiresAt };
635
+ }
596
636
  /** Run one integration operation through the Signet; every call is a new request id. */
597
637
  async execute(
598
638
  request: Omit<AccessRequest, 'requestId' | 'signetId'>,