@inixiative/signet 0.4.6 → 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 +355 -29
- package/src/index.ts +2 -0
- package/src/installationPairing.ts +17 -4
- 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'>,
|
package/src/contracts.ts
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import {
|
|
3
|
+
foundryIdSchema,
|
|
4
|
+
narrowsThreadsLens,
|
|
5
|
+
type ThreadOperation,
|
|
6
|
+
threadFloorActionSchema,
|
|
7
|
+
threadIdSchema,
|
|
8
|
+
threadInputSchemas,
|
|
9
|
+
threadsLensSchema,
|
|
10
|
+
threadsResourceKind,
|
|
11
|
+
threadTextLimit,
|
|
12
|
+
} from './threads';
|
|
13
|
+
|
|
14
|
+
export * from './threads';
|
|
2
15
|
|
|
3
16
|
const token43 = /^[A-Za-z0-9_-]{43}$/;
|
|
4
17
|
|
|
@@ -37,10 +50,50 @@ export const signetKeySchema = publicClientKeySchema.extend({
|
|
|
37
50
|
export type PublicClientKey = z.infer<typeof publicClientKeySchema>;
|
|
38
51
|
export type SignetKey = z.infer<typeof signetKeySchema>;
|
|
39
52
|
|
|
40
|
-
|
|
53
|
+
/** The resource kind of a hosted Archive library: one per Archive integration. */
|
|
54
|
+
export const archiveLibraryResourceKind = 'archiveLibrary';
|
|
55
|
+
/** Which documents and fields of an `archiveLibrary` a grant reaches; `{}` reaches all of them. */
|
|
56
|
+
export const archiveLensSchema = z.strictObject({
|
|
41
57
|
documentIds: z.array(archiveDocumentIdSchema).max(1000).optional(),
|
|
42
58
|
fields: z.array(archiveFieldSchema).max(6).optional(),
|
|
43
59
|
});
|
|
60
|
+
export type ArchiveLens = z.infer<typeof archiveLensSchema>;
|
|
61
|
+
/** Every other resource kind takes no lens. */
|
|
62
|
+
export const emptyLensSchema = z.strictObject({});
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A grant's lens. `signetResourceSchema` names a resource by id with no `kind`, so the contract can
|
|
66
|
+
* only hold a union of the strict shapes; which shape a grant may carry depends on its resource's
|
|
67
|
+
* kind, and Kingdom checks that with `resourceLensSchema` against the kind it stores (review R9).
|
|
68
|
+
* `{}` parses as the empty Archive lens: "no lens", valid for every kind but `threads`.
|
|
69
|
+
*/
|
|
70
|
+
export const signetLensSchema = z.union([archiveLensSchema, threadsLensSchema]);
|
|
71
|
+
export type SignetLens = z.infer<typeof signetLensSchema>;
|
|
72
|
+
|
|
73
|
+
/** The lens shape a resource of `kind` takes. */
|
|
74
|
+
export const resourceLensSchema = (kind: string) =>
|
|
75
|
+
kind === archiveLibraryResourceKind
|
|
76
|
+
? archiveLensSchema
|
|
77
|
+
: kind === threadsResourceKind
|
|
78
|
+
? threadsLensSchema
|
|
79
|
+
: emptyLensSchema;
|
|
80
|
+
|
|
81
|
+
const subset = <T>(granted: T[] | undefined, requested: T[] | undefined) =>
|
|
82
|
+
!requested || (granted !== undefined && granted.every((item) => requested.includes(item)));
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* `granted` reaches nothing `requested` doesn't, for lenses of the same resource. Approving someone
|
|
86
|
+
* else's request may only narrow; an owner revising their own grant may widen (review U3). A lens of
|
|
87
|
+
* another shape than the request's is refused.
|
|
88
|
+
*/
|
|
89
|
+
export const narrowsSignetLens = (granted: SignetLens, requested: SignetLens): boolean => {
|
|
90
|
+
if ('threads' in granted || 'threads' in requested)
|
|
91
|
+
return 'threads' in granted && 'threads' in requested && narrowsThreadsLens(granted, requested);
|
|
92
|
+
return (
|
|
93
|
+
subset(granted.documentIds, requested.documentIds) && subset(granted.fields, requested.fields)
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
44
97
|
export const signetResourceSchema = z.strictObject({
|
|
45
98
|
resourceId: z.uuid(),
|
|
46
99
|
operations: z.array(z.string().min(1).max(80)).min(1).max(100),
|
|
@@ -51,7 +104,6 @@ export const signetRecipientSchema = z.discriminatedUnion('recipientModel', [
|
|
|
51
104
|
z.strictObject({ recipientModel: z.literal('Integration'), integrationId: z.uuid() }),
|
|
52
105
|
]);
|
|
53
106
|
export const signetLifecycleSchema = z.enum(['request', 'task', 'ongoing']);
|
|
54
|
-
export type SignetLens = z.infer<typeof signetLensSchema>;
|
|
55
107
|
export type SignetRecipient = z.infer<typeof signetRecipientSchema>;
|
|
56
108
|
export type SignetResource = z.infer<typeof signetResourceSchema>;
|
|
57
109
|
|
|
@@ -95,11 +147,25 @@ const signetTerms = {
|
|
|
95
147
|
/** A local Foundry, Archive or Oracle known to Kingdom by its key. */
|
|
96
148
|
export const installationKindSchema = z.enum(['foundry', 'archive', 'oracle']);
|
|
97
149
|
export type InstallationKind = z.infer<typeof installationKindSchema>;
|
|
150
|
+
/**
|
|
151
|
+
* A resource asked for by kind, and optionally name, when the asker does not know its id: an
|
|
152
|
+
* Installation registering, or a party asking to join a Foundry's `threads` (one per Foundry, so the
|
|
153
|
+
* integration id names it). Kingdom resolves it to the resource's id before anyone reviews it.
|
|
154
|
+
*/
|
|
155
|
+
export const requestedResourceKindSchema = z.strictObject({
|
|
156
|
+
resourceKind: z.string().min(1).max(80),
|
|
157
|
+
resourceName: z.string().trim().min(1).max(120).optional(),
|
|
158
|
+
operations: z.array(z.string().min(1).max(80)).min(1).max(100),
|
|
159
|
+
lens: signetLensSchema.default({}),
|
|
160
|
+
});
|
|
161
|
+
export const requestedResourceSchema = z.union([signetResourceSchema, requestedResourceKindSchema]);
|
|
162
|
+
/** What a party asks for on an integration: resources by id or by kind (review R8). */
|
|
98
163
|
export const signetProposalSchema = z.strictObject({
|
|
99
164
|
...signetTerms,
|
|
100
165
|
integrationId: z.uuid(),
|
|
101
|
-
resources: z.array(
|
|
166
|
+
resources: z.array(requestedResourceSchema).min(1).max(100),
|
|
102
167
|
});
|
|
168
|
+
/** The terms as reviewed and issued: every resource named by id. */
|
|
103
169
|
export const signetReviewProposalSchema = z.strictObject({
|
|
104
170
|
...signetTerms,
|
|
105
171
|
resources: z.array(signetResourceSchema).max(100),
|
|
@@ -116,14 +182,6 @@ export const requestedOwnerSchema = z.strictObject({
|
|
|
116
182
|
organization: z.string().trim().min(1).max(120).optional(),
|
|
117
183
|
spaceId: z.uuid().optional(),
|
|
118
184
|
});
|
|
119
|
-
/** A resource asked for by kind, and optionally name, when the Installation does not know its id. */
|
|
120
|
-
export const requestedResourceKindSchema = z.strictObject({
|
|
121
|
-
resourceKind: z.string().min(1).max(80),
|
|
122
|
-
resourceName: z.string().trim().min(1).max(120).optional(),
|
|
123
|
-
operations: z.array(z.string().min(1).max(80)).min(1).max(100),
|
|
124
|
-
lens: signetLensSchema.default({}),
|
|
125
|
-
});
|
|
126
|
-
export const requestedResourceSchema = z.union([signetResourceSchema, requestedResourceKindSchema]);
|
|
127
185
|
/** What an Installation asks for: the owner it expects and the resources and operations it needs. */
|
|
128
186
|
export const registrationRequestSchema = z.strictObject({
|
|
129
187
|
...signetTerms,
|
|
@@ -132,10 +190,7 @@ export const registrationRequestSchema = z.strictObject({
|
|
|
132
190
|
resources: z.array(requestedResourceSchema).max(100),
|
|
133
191
|
});
|
|
134
192
|
/** A person asks for a Signet on an integration, to be presented with `publicKey`. */
|
|
135
|
-
export const signetRequestSchema =
|
|
136
|
-
...signetTerms,
|
|
137
|
-
integrationId: z.uuid(),
|
|
138
|
-
resources: z.array(signetResourceSchema).min(1).max(100),
|
|
193
|
+
export const signetRequestSchema = signetProposalSchema.extend({
|
|
139
194
|
publicKey: publicClientKeySchema,
|
|
140
195
|
});
|
|
141
196
|
export const signetRequestResponseSchema = z.object({
|
|
@@ -172,10 +227,18 @@ export const requestRegistrationResponseSchema = z.object({
|
|
|
172
227
|
/** Where a person reviews the registration in Kingdom's web app. */
|
|
173
228
|
reviewUrl: kingdomLinkSchema,
|
|
174
229
|
});
|
|
230
|
+
const inquiryStatusSchema = z.enum([
|
|
231
|
+
'draft',
|
|
232
|
+
'sent',
|
|
233
|
+
'changesRequested',
|
|
234
|
+
'approved',
|
|
235
|
+
'denied',
|
|
236
|
+
'canceled',
|
|
237
|
+
]);
|
|
175
238
|
export const installationInquirySchema = z.object({
|
|
176
239
|
id: z.uuid(),
|
|
177
240
|
type: z.string(),
|
|
178
|
-
status:
|
|
241
|
+
status: inquiryStatusSchema,
|
|
179
242
|
createdAt: z.iso.datetime(),
|
|
180
243
|
expiresAt: z.iso.datetime().nullable(),
|
|
181
244
|
owner: ownerRefSchema.nullable(),
|
|
@@ -184,23 +247,90 @@ export const installationInquirySchema = z.object({
|
|
|
184
247
|
signetId: z.uuid().nullable(),
|
|
185
248
|
deliverBefore: z.iso.datetime().nullable(),
|
|
186
249
|
});
|
|
250
|
+
/**
|
|
251
|
+
* A pending `signetRequest` whose target is one of the Installation's integrations: a party asking
|
|
252
|
+
* to join a Foundry's threads. The owner reviews it in Kingdom at `reviewUrl`.
|
|
253
|
+
*/
|
|
254
|
+
export const hostedSignetRequestSchema = z.object({
|
|
255
|
+
id: z.uuid(),
|
|
256
|
+
status: inquiryStatusSchema,
|
|
257
|
+
createdAt: z.iso.datetime(),
|
|
258
|
+
expiresAt: z.iso.datetime().nullable(),
|
|
259
|
+
/** The Installation's integration the request targets. */
|
|
260
|
+
integrationId: z.uuid(),
|
|
261
|
+
requester: z.object({ sourceModel: z.string(), id: z.uuid(), displayName: z.string() }),
|
|
262
|
+
terms: signetReviewProposalSchema,
|
|
263
|
+
reviewUrl: kingdomLinkSchema,
|
|
264
|
+
});
|
|
265
|
+
export type HostedSignetRequest = z.infer<typeof hostedSignetRequestSchema>;
|
|
187
266
|
export const installationInquiriesResponseSchema = z.object({
|
|
188
267
|
pending: z.object({ reviewCode: z.string(), expiresAt: z.iso.datetime() }).nullable(),
|
|
189
268
|
declinedAt: z.iso.datetime().nullable(),
|
|
190
269
|
inquiries: z.array(installationInquirySchema),
|
|
270
|
+
hostedSignetRequests: z.array(hostedSignetRequestSchema),
|
|
191
271
|
});
|
|
192
272
|
export const inquiryReferenceSchema = z.strictObject({ inquiryId: z.uuid() });
|
|
273
|
+
|
|
274
|
+
/** One resource a Signet reaches, with its stored kind, so its lens can be checked against it. */
|
|
275
|
+
export const signetGrantSchema = z.object({
|
|
276
|
+
resourceId: z.uuid(),
|
|
277
|
+
kind: z.string().min(1).max(80),
|
|
278
|
+
operations: z.array(z.string().min(1).max(80)),
|
|
279
|
+
lens: signetLensSchema,
|
|
280
|
+
});
|
|
281
|
+
export type SignetGrant = z.infer<typeof signetGrantSchema>;
|
|
282
|
+
const grantsSchema = z.array(signetGrantSchema);
|
|
283
|
+
/** Whether a grant's lens is the shape its resource's kind takes. */
|
|
284
|
+
export const grantLensFitsKind = (grant: Pick<SignetGrant, 'kind' | 'lens'>) =>
|
|
285
|
+
resourceLensSchema(grant.kind).safeParse(grant.lens).success;
|
|
286
|
+
|
|
287
|
+
/** Who a Signet is issued to. Recipients run broad (2026-10-18); Kingdom widens issuing in K0. */
|
|
288
|
+
export const signetRecipientModelSchema = z.enum([
|
|
289
|
+
'User',
|
|
290
|
+
'OrganizationUser',
|
|
291
|
+
'Organization',
|
|
292
|
+
'Space',
|
|
293
|
+
'SpaceUser',
|
|
294
|
+
'Integration',
|
|
295
|
+
]);
|
|
296
|
+
export const installationSignetSchema = z.object({
|
|
297
|
+
signetId: z.uuid(),
|
|
298
|
+
integrationId: z.uuid(),
|
|
299
|
+
name: z.string(),
|
|
300
|
+
/** The enrollment that is current for this installation's key, if any. */
|
|
301
|
+
enrollmentId: z.uuid().nullable(),
|
|
302
|
+
owner: ownerRefSchema,
|
|
303
|
+
/** What the Signet reaches, so a holder knows, for example, which threads it may open. */
|
|
304
|
+
resources: grantsSchema,
|
|
305
|
+
});
|
|
306
|
+
/**
|
|
307
|
+
* A Signet issued on one of the Installation's integrations: a member of a Foundry's conversations.
|
|
308
|
+
* For display and for cancelling a member's queued work once it is gone, never as an admission
|
|
309
|
+
* gate: Kingdom admits every call.
|
|
310
|
+
*/
|
|
311
|
+
export const hostedSignetSchema = z.object({
|
|
312
|
+
signetId: z.uuid(),
|
|
313
|
+
/** The Installation's integration the Signet is issued on. */
|
|
314
|
+
integrationId: z.uuid(),
|
|
315
|
+
name: z.string(),
|
|
316
|
+
/** The current revision; the host compares it with the `acceptedRevision` frames carry. */
|
|
317
|
+
revision: z.number().int().min(1),
|
|
318
|
+
lifecycle: signetLifecycleSchema,
|
|
319
|
+
expiresAt: z.iso.datetime().nullable(),
|
|
320
|
+
maxRequests: z.number().int().nullable(),
|
|
321
|
+
maxConcurrent: z.number().int(),
|
|
322
|
+
recipient: z.object({
|
|
323
|
+
recipientModel: signetRecipientModelSchema,
|
|
324
|
+
id: z.uuid(),
|
|
325
|
+
displayName: z.string(),
|
|
326
|
+
}),
|
|
327
|
+
resources: grantsSchema,
|
|
328
|
+
});
|
|
329
|
+
export type InstallationSignet = z.infer<typeof installationSignetSchema>;
|
|
330
|
+
export type HostedSignet = z.infer<typeof hostedSignetSchema>;
|
|
193
331
|
export const installationSignetsResponseSchema = z.object({
|
|
194
|
-
signets: z.array(
|
|
195
|
-
|
|
196
|
-
signetId: z.uuid(),
|
|
197
|
-
integrationId: z.uuid(),
|
|
198
|
-
name: z.string(),
|
|
199
|
-
/** The enrollment that is current for this installation's key, if any. */
|
|
200
|
-
enrollmentId: z.uuid().nullable(),
|
|
201
|
-
owner: ownerRefSchema,
|
|
202
|
-
}),
|
|
203
|
-
),
|
|
332
|
+
signets: z.array(installationSignetSchema),
|
|
333
|
+
hostedSignets: z.array(hostedSignetSchema),
|
|
204
334
|
});
|
|
205
335
|
export const enrollInstallationSignetSchema = z.strictObject({ signetId: z.uuid() });
|
|
206
336
|
export const rotateInstallationKeySchema = z.strictObject({
|
|
@@ -209,6 +339,13 @@ export const rotateInstallationKeySchema = z.strictObject({
|
|
|
209
339
|
});
|
|
210
340
|
export type InstallationInquiry = z.infer<typeof installationInquirySchema>;
|
|
211
341
|
|
|
342
|
+
export const signetDescriptionResourceSchema = z.object({
|
|
343
|
+
id: z.uuid(),
|
|
344
|
+
name: z.string(),
|
|
345
|
+
kind: z.string(),
|
|
346
|
+
integrationId: z.uuid(),
|
|
347
|
+
lens: signetLensSchema,
|
|
348
|
+
});
|
|
212
349
|
export const signetDescriptionSchema = z.object({
|
|
213
350
|
signetId: z.uuid(),
|
|
214
351
|
integrationId: z.uuid(),
|
|
@@ -223,9 +360,7 @@ export const signetDescriptionSchema = z.object({
|
|
|
223
360
|
z.object({
|
|
224
361
|
key: z.string(),
|
|
225
362
|
name: z.string(),
|
|
226
|
-
resources: z.array(
|
|
227
|
-
z.object({ id: z.uuid(), name: z.string(), kind: z.string(), integrationId: z.uuid() }),
|
|
228
|
-
),
|
|
363
|
+
resources: z.array(signetDescriptionResourceSchema),
|
|
229
364
|
}),
|
|
230
365
|
),
|
|
231
366
|
});
|
|
@@ -271,6 +406,14 @@ export const accessInputSchema = z.strictObject({
|
|
|
271
406
|
.regex(/^[a-f0-9]{64}$/)
|
|
272
407
|
.nullable()
|
|
273
408
|
.optional(),
|
|
409
|
+
threadId: threadIdSchema.optional(),
|
|
410
|
+
postId: z.uuid().optional(),
|
|
411
|
+
text: z.string().min(1).max(threadTextLimit).optional(),
|
|
412
|
+
inReplyTo: foundryIdSchema.optional(),
|
|
413
|
+
afterSeq: z.number().int().min(0).optional(),
|
|
414
|
+
floor: threadFloorActionSchema.optional(),
|
|
415
|
+
promptId: foundryIdSchema.optional(),
|
|
416
|
+
answer: threadInputSchemas['prompt.resolve'].shape.answer.optional(),
|
|
274
417
|
});
|
|
275
418
|
export const accessRequestSchema = z.strictObject({
|
|
276
419
|
requestId: z.uuid(),
|
|
@@ -283,12 +426,195 @@ export const accessRequestSchema = z.strictObject({
|
|
|
283
426
|
});
|
|
284
427
|
export type AccessRequest = z.infer<typeof accessRequestSchema>;
|
|
285
428
|
export const accessResultSchema = z.object({ executionId: z.uuid(), result: z.unknown() });
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* The host-side input of a thread operation, taken from an `access/execute` input: exactly the keys
|
|
432
|
+
* that operation takes (no `resourceId`, no `limit`). Kingdom builds `thread.request` frames with it.
|
|
433
|
+
*/
|
|
434
|
+
export const threadInputFromAccess = <O extends ThreadOperation>(
|
|
435
|
+
operation: O,
|
|
436
|
+
input: AccessRequest['input'],
|
|
437
|
+
) => {
|
|
438
|
+
const schema = threadInputSchemas[operation];
|
|
439
|
+
const picked = Object.fromEntries(
|
|
440
|
+
Object.keys(schema.shape).flatMap((key) =>
|
|
441
|
+
key in input ? [[key, input[key as keyof typeof input]]] : [],
|
|
442
|
+
),
|
|
443
|
+
);
|
|
444
|
+
return schema.safeParse(picked) as z.ZodSafeParseResult<z.infer<(typeof threadInputSchemas)[O]>>;
|
|
445
|
+
};
|
|
446
|
+
|
|
286
447
|
/** What Kingdom pushes over the Installation socket, and what a poll of both reads assembles. */
|
|
287
448
|
export const installationSnapshotSchema = installationInquiriesResponseSchema.extend(
|
|
288
449
|
installationSignetsResponseSchema.shape,
|
|
289
450
|
);
|
|
451
|
+
|
|
452
|
+
/** An entry a reader dropped instead of failing the whole read, and why (review U1). */
|
|
453
|
+
export type DroppedEntry = { list: string; index: number; id: string | null; reason: string };
|
|
454
|
+
export type InstallationSnapshot = z.infer<typeof installationSnapshotSchema> & {
|
|
455
|
+
dropped: DroppedEntry[];
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
const entryId = (value: unknown) => {
|
|
459
|
+
if (typeof value !== 'object' || value === null) return null;
|
|
460
|
+
const record = value as Record<string, unknown>;
|
|
461
|
+
const id = record.signetId ?? record.id;
|
|
462
|
+
return typeof id === 'string' ? id.slice(0, 80) : null;
|
|
463
|
+
};
|
|
464
|
+
/**
|
|
465
|
+
* Parse each entry on its own and drop the ones that don't parse. A Kingdom ahead of this package
|
|
466
|
+
* (a selector form it doesn't know, a new field shape) then costs only those entries: never the
|
|
467
|
+
* Installation's other grants or its own held Signets. A missing list reads as empty, reported.
|
|
468
|
+
*/
|
|
469
|
+
export function parseEntries<T>(
|
|
470
|
+
list: string,
|
|
471
|
+
value: unknown,
|
|
472
|
+
schema: z.ZodType<T>,
|
|
473
|
+
dropped: DroppedEntry[],
|
|
474
|
+
check: (entry: T) => string | null = () => null,
|
|
475
|
+
): T[] {
|
|
476
|
+
if (!Array.isArray(value)) {
|
|
477
|
+
dropped.push({ list, index: -1, id: null, reason: 'missing or not a list' });
|
|
478
|
+
return [];
|
|
479
|
+
}
|
|
480
|
+
return value.flatMap((raw, index) => {
|
|
481
|
+
const parsed = schema.safeParse(raw);
|
|
482
|
+
const reason = parsed.success
|
|
483
|
+
? check(parsed.data)
|
|
484
|
+
: parsed.error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`).join('; ');
|
|
485
|
+
if (reason === null && parsed.success) return [parsed.data];
|
|
486
|
+
dropped.push({ list, index, id: entryId(raw), reason: (reason ?? 'invalid').slice(0, 500) });
|
|
487
|
+
return [];
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const lensFits = (grant: Pick<SignetGrant, 'kind' | 'lens'>) =>
|
|
492
|
+
grantLensFitsKind(grant) ? null : `a ${grant.kind} resource carries a lens of another shape`;
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Signets read per entry, and their resources per entry inside them: a resource this package can't
|
|
496
|
+
* read (a newer lens, a lens that doesn't fit its kind) is dropped and reported, and the Signet is
|
|
497
|
+
* kept, so the Installation never loses a held Signet (or mistakes a member for revoked) over one
|
|
498
|
+
* resource.
|
|
499
|
+
*/
|
|
500
|
+
function parseSignetsWithGrants<S extends z.ZodObject<{ resources: typeof grantsSchema }>>(
|
|
501
|
+
list: string,
|
|
502
|
+
value: unknown,
|
|
503
|
+
schema: S,
|
|
504
|
+
dropped: DroppedEntry[],
|
|
505
|
+
): z.infer<S>[] {
|
|
506
|
+
const identity = schema.omit({ resources: true }).extend({ resources: z.unknown().optional() });
|
|
507
|
+
return parseEntries(list, value, identity, dropped).map(
|
|
508
|
+
(entry) =>
|
|
509
|
+
({
|
|
510
|
+
...entry,
|
|
511
|
+
resources: parseEntries(
|
|
512
|
+
`${list}.${entryId(entry)}.resources`,
|
|
513
|
+
entry.resources,
|
|
514
|
+
signetGrantSchema,
|
|
515
|
+
dropped,
|
|
516
|
+
lensFits,
|
|
517
|
+
),
|
|
518
|
+
}) as z.infer<S>,
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const inquiriesScalars = installationInquiriesResponseSchema.pick({
|
|
523
|
+
pending: true,
|
|
524
|
+
declinedAt: true,
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
/** `installationInquiries`, per entry. */
|
|
528
|
+
export function parseInstallationInquiries(value: unknown, dropped: DroppedEntry[] = []) {
|
|
529
|
+
const record = z.object({}).loose().parse(value);
|
|
530
|
+
return {
|
|
531
|
+
...inquiriesScalars.parse(record),
|
|
532
|
+
inquiries: parseEntries('inquiries', record.inquiries, installationInquirySchema, dropped),
|
|
533
|
+
hostedSignetRequests: parseEntries(
|
|
534
|
+
'hostedSignetRequests',
|
|
535
|
+
record.hostedSignetRequests,
|
|
536
|
+
hostedSignetRequestSchema,
|
|
537
|
+
dropped,
|
|
538
|
+
),
|
|
539
|
+
dropped,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/** `installationSignets`, per Signet and per resource inside it. */
|
|
544
|
+
export function parseInstallationSignets(value: unknown, dropped: DroppedEntry[] = []) {
|
|
545
|
+
const record = z.object({}).loose().parse(value);
|
|
546
|
+
return {
|
|
547
|
+
signets: parseSignetsWithGrants('signets', record.signets, installationSignetSchema, dropped),
|
|
548
|
+
hostedSignets: parseSignetsWithGrants(
|
|
549
|
+
'hostedSignets',
|
|
550
|
+
record.hostedSignets,
|
|
551
|
+
hostedSignetSchema,
|
|
552
|
+
dropped,
|
|
553
|
+
),
|
|
554
|
+
dropped,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/** The Installation socket's snapshot frame (or a poll of both reads), per entry. */
|
|
559
|
+
export function parseInstallationSnapshot(value: unknown): InstallationSnapshot {
|
|
560
|
+
const dropped: DroppedEntry[] = [];
|
|
561
|
+
const { dropped: _inquiries, ...inquiries } = parseInstallationInquiries(value, dropped);
|
|
562
|
+
const { dropped: _signets, ...signets } = parseInstallationSignets(value, dropped);
|
|
563
|
+
return { ...inquiries, ...signets, dropped };
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
export type ParsedSignetDescription = SignetDescription & { dropped: DroppedEntry[] };
|
|
567
|
+
|
|
568
|
+
/** `access/describe`, per entry: a resource whose lens doesn't parse, or doesn't fit its kind, is dropped. */
|
|
569
|
+
export function parseSignetDescription(value: unknown): ParsedSignetDescription {
|
|
570
|
+
const dropped: DroppedEntry[] = [];
|
|
571
|
+
const record = z
|
|
572
|
+
.object({ operations: z.array(z.object({}).loose()) })
|
|
573
|
+
.loose()
|
|
574
|
+
.parse(value);
|
|
575
|
+
const operations = record.operations.map((operation, index) => ({
|
|
576
|
+
...signetDescriptionSchema.shape.operations.element.omit({ resources: true }).parse(operation),
|
|
577
|
+
resources: parseEntries(
|
|
578
|
+
`operations.${index}.resources`,
|
|
579
|
+
operation.resources,
|
|
580
|
+
signetDescriptionResourceSchema,
|
|
581
|
+
dropped,
|
|
582
|
+
(resource) => (grantLensFitsKind(resource) ? null : 'lens does not fit the resource kind'),
|
|
583
|
+
),
|
|
584
|
+
}));
|
|
585
|
+
return { ...signetDescriptionSchema.parse({ ...record, operations }), dropped };
|
|
586
|
+
}
|
|
587
|
+
|
|
290
588
|
/** The path a socket proof is bound to (POST), though the socket itself opens at the API origin. */
|
|
291
589
|
export const installationSocketAction = 'installationSocket';
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* A Signet presenter's socket (design §5.6.1, review Round 4). Like the Installation socket: a DPoP
|
|
593
|
+
* proof bound to `POST /api/v1/access/signetSocket`, on a single-use Kingdom nonce, with `ath` over
|
|
594
|
+
* the access token. The socket then carries one actor, that Signet's presentation, until the token
|
|
595
|
+
* expires; `renewPresentation` (a new token, a new proof) extends it.
|
|
596
|
+
*/
|
|
597
|
+
export const signetSocketAction = 'signetSocket';
|
|
598
|
+
const presentation = {
|
|
599
|
+
accessToken: z.string().regex(accessTokenPattern),
|
|
600
|
+
proof: z.string().max(8192),
|
|
601
|
+
};
|
|
602
|
+
export const authenticateSignetFrameSchema = z.strictObject({
|
|
603
|
+
action: z.literal('authenticateSignet'),
|
|
604
|
+
...presentation,
|
|
605
|
+
});
|
|
606
|
+
export const renewPresentationFrameSchema = z.strictObject({
|
|
607
|
+
action: z.literal('renewPresentation'),
|
|
608
|
+
...presentation,
|
|
609
|
+
});
|
|
610
|
+
/** Kingdom's answer to either: the presentation the socket now carries, and until when. */
|
|
611
|
+
export const signetPresentedFrameSchema = z.object({
|
|
612
|
+
type: z.literal('signet'),
|
|
613
|
+
signetId: z.uuid(),
|
|
614
|
+
enrollmentId: z.uuid(),
|
|
615
|
+
expiresAt: z.iso.datetime(),
|
|
616
|
+
});
|
|
617
|
+
export const signetRejectedFrameSchema = z.object({ type: z.literal('signetRejected') });
|
|
292
618
|
/** An installation hands Kingdom the credential Kingdom uses to reach it (a Foundry's tunnel token). */
|
|
293
619
|
export const setInstallationCredentialSchema = z.strictObject({
|
|
294
620
|
integrationId: z.uuid(),
|