@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/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({
|
|
@@ -164,14 +219,26 @@ export const registerInstallationSchema = z
|
|
|
164
219
|
});
|
|
165
220
|
export const registerInstallationResponseSchema = z.object({ installationId: z.uuid() });
|
|
166
221
|
export const requestRegistrationSchema = z.strictObject({ terms: registrationRequestSchema });
|
|
222
|
+
/** A link into Kingdom's web app that callers open, so http(s) only. */
|
|
223
|
+
const kingdomLinkSchema = z.url({ protocol: /^https?$/ });
|
|
167
224
|
export const requestRegistrationResponseSchema = z.object({
|
|
168
225
|
reviewCode: z.string(),
|
|
169
226
|
expiresAt: z.iso.datetime(),
|
|
227
|
+
/** Where a person reviews the registration in Kingdom's web app. */
|
|
228
|
+
reviewUrl: kingdomLinkSchema,
|
|
170
229
|
});
|
|
230
|
+
const inquiryStatusSchema = z.enum([
|
|
231
|
+
'draft',
|
|
232
|
+
'sent',
|
|
233
|
+
'changesRequested',
|
|
234
|
+
'approved',
|
|
235
|
+
'denied',
|
|
236
|
+
'canceled',
|
|
237
|
+
]);
|
|
171
238
|
export const installationInquirySchema = z.object({
|
|
172
239
|
id: z.uuid(),
|
|
173
240
|
type: z.string(),
|
|
174
|
-
status:
|
|
241
|
+
status: inquiryStatusSchema,
|
|
175
242
|
createdAt: z.iso.datetime(),
|
|
176
243
|
expiresAt: z.iso.datetime().nullable(),
|
|
177
244
|
owner: ownerRefSchema.nullable(),
|
|
@@ -180,23 +247,90 @@ export const installationInquirySchema = z.object({
|
|
|
180
247
|
signetId: z.uuid().nullable(),
|
|
181
248
|
deliverBefore: z.iso.datetime().nullable(),
|
|
182
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>;
|
|
183
266
|
export const installationInquiriesResponseSchema = z.object({
|
|
184
267
|
pending: z.object({ reviewCode: z.string(), expiresAt: z.iso.datetime() }).nullable(),
|
|
185
268
|
declinedAt: z.iso.datetime().nullable(),
|
|
186
269
|
inquiries: z.array(installationInquirySchema),
|
|
270
|
+
hostedSignetRequests: z.array(hostedSignetRequestSchema),
|
|
187
271
|
});
|
|
188
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>;
|
|
189
331
|
export const installationSignetsResponseSchema = z.object({
|
|
190
|
-
signets: z.array(
|
|
191
|
-
|
|
192
|
-
signetId: z.uuid(),
|
|
193
|
-
integrationId: z.uuid(),
|
|
194
|
-
name: z.string(),
|
|
195
|
-
/** The enrollment that is current for this installation's key, if any. */
|
|
196
|
-
enrollmentId: z.uuid().nullable(),
|
|
197
|
-
owner: ownerRefSchema,
|
|
198
|
-
}),
|
|
199
|
-
),
|
|
332
|
+
signets: z.array(installationSignetSchema),
|
|
333
|
+
hostedSignets: z.array(hostedSignetSchema),
|
|
200
334
|
});
|
|
201
335
|
export const enrollInstallationSignetSchema = z.strictObject({ signetId: z.uuid() });
|
|
202
336
|
export const rotateInstallationKeySchema = z.strictObject({
|
|
@@ -205,6 +339,13 @@ export const rotateInstallationKeySchema = z.strictObject({
|
|
|
205
339
|
});
|
|
206
340
|
export type InstallationInquiry = z.infer<typeof installationInquirySchema>;
|
|
207
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
|
+
});
|
|
208
349
|
export const signetDescriptionSchema = z.object({
|
|
209
350
|
signetId: z.uuid(),
|
|
210
351
|
integrationId: z.uuid(),
|
|
@@ -219,14 +360,36 @@ export const signetDescriptionSchema = z.object({
|
|
|
219
360
|
z.object({
|
|
220
361
|
key: z.string(),
|
|
221
362
|
name: z.string(),
|
|
222
|
-
resources: z.array(
|
|
223
|
-
z.object({ id: z.uuid(), name: z.string(), kind: z.string(), integrationId: z.uuid() }),
|
|
224
|
-
),
|
|
363
|
+
resources: z.array(signetDescriptionResourceSchema),
|
|
225
364
|
}),
|
|
226
365
|
),
|
|
227
366
|
});
|
|
228
367
|
export type SignetDescription = z.infer<typeof signetDescriptionSchema>;
|
|
229
368
|
|
|
369
|
+
const holderPersonSchema = z.object({
|
|
370
|
+
id: z.uuid(),
|
|
371
|
+
name: z.string().nullable(),
|
|
372
|
+
email: z.string(),
|
|
373
|
+
});
|
|
374
|
+
/**
|
|
375
|
+
* `access/describe`'s `holder` for an Integration-held Signet (an Installation such as a Foundry):
|
|
376
|
+
* the integration, its owner, who issued the Signet, and where to manage it in Kingdom.
|
|
377
|
+
*/
|
|
378
|
+
export const signetHolderSchema = z.object({
|
|
379
|
+
integration: z.object({ id: z.uuid(), name: z.string(), provider: z.string().nullable() }),
|
|
380
|
+
owner: z.object({
|
|
381
|
+
ownerModel: ownerModelSchema,
|
|
382
|
+
name: z.string().nullable(),
|
|
383
|
+
organization: z.object({ id: z.uuid(), name: z.string(), slug: z.string() }).nullable(),
|
|
384
|
+
space: z.object({ id: z.uuid(), name: z.string() }).nullable(),
|
|
385
|
+
user: holderPersonSchema.nullable(),
|
|
386
|
+
}),
|
|
387
|
+
issuedBy: holderPersonSchema.nullable(),
|
|
388
|
+
webUrl: kingdomLinkSchema,
|
|
389
|
+
manageUrl: kingdomLinkSchema,
|
|
390
|
+
});
|
|
391
|
+
export type SignetHolder = z.infer<typeof signetHolderSchema>;
|
|
392
|
+
|
|
230
393
|
/** Operation-specific fields; Kingdom validates each against the operation it names. */
|
|
231
394
|
export const accessInputSchema = z.strictObject({
|
|
232
395
|
resourceId: z.uuid(),
|
|
@@ -243,6 +406,14 @@ export const accessInputSchema = z.strictObject({
|
|
|
243
406
|
.regex(/^[a-f0-9]{64}$/)
|
|
244
407
|
.nullable()
|
|
245
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(),
|
|
246
417
|
});
|
|
247
418
|
export const accessRequestSchema = z.strictObject({
|
|
248
419
|
requestId: z.uuid(),
|
|
@@ -255,12 +426,195 @@ export const accessRequestSchema = z.strictObject({
|
|
|
255
426
|
});
|
|
256
427
|
export type AccessRequest = z.infer<typeof accessRequestSchema>;
|
|
257
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
|
+
|
|
258
447
|
/** What Kingdom pushes over the Installation socket, and what a poll of both reads assembles. */
|
|
259
448
|
export const installationSnapshotSchema = installationInquiriesResponseSchema.extend(
|
|
260
449
|
installationSignetsResponseSchema.shape,
|
|
261
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
|
+
|
|
262
588
|
/** The path a socket proof is bound to (POST), though the socket itself opens at the API origin. */
|
|
263
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') });
|
|
264
618
|
/** An installation hands Kingdom the credential Kingdom uses to reach it (a Foundry's tunnel token). */
|
|
265
619
|
export const setInstallationCredentialSchema = z.strictObject({
|
|
266
620
|
integrationId: z.uuid(),
|