@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 +30 -1
- package/package.json +2 -2
- package/src/client.ts +50 -10
- package/src/contracts.ts +383 -29
- package/src/index.ts +2 -0
- package/src/installationPairing.ts +137 -26
- package/src/installationSocket.ts +381 -10
- package/src/signetSocket.ts +214 -0
- package/src/threadStreams.ts +315 -0
- package/src/threads.ts +431 -0
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.
|
|
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.
|
|
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
|
-
/**
|
|
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(
|
|
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
|
-
/**
|
|
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
|
-
) =>
|
|
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
|
-
|
|
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
|
|
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'>,
|