@inixiative/signet 0.1.0 → 0.2.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inixiative/signet",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
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.49",
37
+ "@inixiative/config": "^0.5.55",
38
38
  "@types/bun": "1.4.2",
39
39
  "lefthook": "2.1.9",
40
40
  "typescript": "6.0.3",
package/src/client.ts CHANGED
@@ -6,18 +6,22 @@ import {
6
6
  accessResultSchema,
7
7
  accessTokenPattern,
8
8
  collectedSignetSchema,
9
- type IntegrationPairingProposal,
9
+ type InstallationKind,
10
+ installationInquiriesResponseSchema,
11
+ installationSignetsResponseSchema,
10
12
  nonceSchema,
11
13
  ownerRefSchema,
12
14
  publicClientKeySchema,
15
+ type RegistrationTerms,
16
+ registerInstallationResponseSchema,
13
17
  renewalCredentialPattern,
18
+ requestRegistrationResponseSchema,
14
19
  signetDescriptionSchema,
15
20
  signetKeySchema,
16
21
  signetLifecycleSchema,
17
- signetRequestResponseSchema,
18
22
  } from './contracts';
19
23
  import { readPrivateJson, writePrivateJson } from './files';
20
- import { createSignetProof } from './proof';
24
+ import { createSignetProof, keyThumbprint } from './proof';
21
25
 
22
26
  /** A Kingdom API origin: HTTPS, or HTTP on loopback, with no path or credentials. */
23
27
  export const kingdomUrl = (value: string) => {
@@ -201,48 +205,109 @@ export async function signetProof(
201
205
  });
202
206
  }
203
207
 
204
- export type PairingTerms = Omit<IntegrationPairingProposal, 'lifecycle' | 'taskId'>;
205
-
206
- /** Ask Kingdom to pair this device as an integration; the owner approves the review code. */
207
- export async function requestPairing(
208
+ /** A call an Installation makes as itself: no access token, a DPoP proof by its key. */
209
+ async function installationPost<T>(
208
210
  url: string,
209
211
  keyFile: string,
210
- terms: PairingTerms,
212
+ action: string,
213
+ body: unknown,
214
+ schema: z.ZodType<T>,
211
215
  options: SignetRequestOptions = {},
212
- ) {
213
- const publicKey = signetPublicKey(await readPrivateJson(keyFile));
214
- return signetRequestResponseSchema.parse(
216
+ ): Promise<T> {
217
+ return schema.parse(
215
218
  await signetPost(
216
219
  url,
217
- 'requestSignet',
218
- { ...terms, lifecycle: 'ongoing', publicKey },
219
- { DPoP: await signetProof(url, 'requestSignet', keyFile, undefined, options) },
220
+ action,
221
+ body,
222
+ { DPoP: await signetProof(url, action, keyFile, undefined, options) },
220
223
  undefined,
221
224
  options,
222
225
  ),
223
226
  );
224
227
  }
225
228
 
229
+ /** Make this installation known to Kingdom. Idempotent for the same key. */
230
+ export const registerInstallation = (
231
+ url: string,
232
+ keyFile: string,
233
+ installation: { kind: InstallationKind; name: string; sourceId?: string },
234
+ options: SignetRequestOptions = {},
235
+ ) =>
236
+ readPrivateJson(keyFile).then((key) =>
237
+ installationPost(
238
+ url,
239
+ keyFile,
240
+ 'registerInstallation',
241
+ { ...installation, publicKey: signetPublicKey(key) },
242
+ registerInstallationResponseSchema,
243
+ options,
244
+ ),
245
+ );
246
+
247
+ /** Ask to be registered as an integration; a person claims the review code in Kingdom. */
248
+ export const requestRegistration = (
249
+ url: string,
250
+ keyFile: string,
251
+ terms: RegistrationTerms,
252
+ options: SignetRequestOptions = {},
253
+ ) =>
254
+ installationPost(
255
+ url,
256
+ keyFile,
257
+ 'requestRegistration',
258
+ { terms },
259
+ requestRegistrationResponseSchema,
260
+ options,
261
+ );
262
+
263
+ /** This installation's pending request and its inquiries. */
264
+ export const installationInquiries = (
265
+ url: string,
266
+ keyFile: string,
267
+ options: SignetRequestOptions = {},
268
+ ) =>
269
+ installationPost(
270
+ url,
271
+ keyFile,
272
+ 'installationInquiries',
273
+ {},
274
+ installationInquiriesResponseSchema,
275
+ options,
276
+ );
277
+
278
+ export const cancelInstallationInquiry = (
279
+ url: string,
280
+ keyFile: string,
281
+ inquiryId: string,
282
+ options: SignetRequestOptions = {},
283
+ ) =>
284
+ installationPost(
285
+ url,
286
+ keyFile,
287
+ 'cancelInstallationInquiry',
288
+ { inquiryId },
289
+ z.object({ id: z.uuid() }),
290
+ options,
291
+ );
292
+
226
293
  /**
227
- * Collect an approved pairing. Returns null while it awaits review (Kingdom answers 409; stop
228
- * polling at the request's expiresAt) and throws once it is denied (403).
294
+ * Collect the Signet an approved inquiry minted. Returns null while it awaits review (Kingdom
295
+ * answers 409) and throws once it is declined, denied or past its delivery deadline (403).
229
296
  */
230
- export async function collectPairing(
297
+ export async function collectSignet(
231
298
  url: string,
232
299
  keyFile: string,
233
- deviceCode: string,
300
+ inquiryId: string,
234
301
  options: SignetRequestOptions = {},
235
302
  ) {
236
303
  try {
237
- return collectedSignetSchema.parse(
238
- await signetPost(
239
- url,
240
- 'collectSignet',
241
- { deviceCode },
242
- { DPoP: await signetProof(url, 'collectSignet', keyFile, undefined, options) },
243
- undefined,
244
- options,
245
- ),
304
+ return await installationPost(
305
+ url,
306
+ keyFile,
307
+ 'collectSignet',
308
+ { inquiryId },
309
+ collectedSignetSchema,
310
+ options,
246
311
  );
247
312
  } catch (error) {
248
313
  if (error instanceof SignetHttpError && error.status === 409) return null;
@@ -250,7 +315,64 @@ export async function collectPairing(
250
315
  }
251
316
  }
252
317
 
253
- /** Write a collected pairing next to its key; the file is what a SignetClient presents. */
318
+ /** The Signets held by this installation's integrations. */
319
+ export const installationSignets = (
320
+ url: string,
321
+ keyFile: string,
322
+ options: SignetRequestOptions = {},
323
+ ) =>
324
+ installationPost(
325
+ url,
326
+ keyFile,
327
+ 'installationSignets',
328
+ {},
329
+ installationSignetsResponseSchema,
330
+ options,
331
+ );
332
+
333
+ /** Enroll this installation's key on a Signet one of its integrations holds. */
334
+ export const enrollInstallationSignet = (
335
+ url: string,
336
+ keyFile: string,
337
+ signetId: string,
338
+ options: SignetRequestOptions = {},
339
+ ) =>
340
+ installationPost(
341
+ url,
342
+ keyFile,
343
+ 'enrollInstallationSignet',
344
+ { signetId },
345
+ collectedSignetSchema,
346
+ options,
347
+ );
348
+
349
+ /** Move this installation to a new key; both keys sign, the new proof bound to the current key. */
350
+ export async function rotateInstallationKey(
351
+ url: string,
352
+ keyFile: string,
353
+ newKeyFile: string,
354
+ options: SignetRequestOptions = {},
355
+ ) {
356
+ const publicKey = signetPublicKey(await readPrivateJson(newKeyFile));
357
+ const currentThumbprint = await keyThumbprint(signetPublicKey(await readPrivateJson(keyFile)));
358
+ const proof = await signetProof(
359
+ url,
360
+ 'rotateInstallationKey',
361
+ newKeyFile,
362
+ currentThumbprint,
363
+ options,
364
+ );
365
+ return installationPost(
366
+ url,
367
+ keyFile,
368
+ 'rotateInstallationKey',
369
+ { publicKey, proof },
370
+ registerInstallationResponseSchema,
371
+ options,
372
+ );
373
+ }
374
+
375
+ /** Write a collected Signet next to its key; the file is what a SignetClient presents. */
254
376
  export async function saveCollectedSignet(
255
377
  credentialFile: string,
256
378
  url: string,
package/src/contracts.ts CHANGED
@@ -5,7 +5,6 @@ const token43 = /^[A-Za-z0-9_-]{43}$/;
5
5
  export const accessTokenPattern = /^kingdom_[A-Za-z0-9_-]{43}$/;
6
6
  export const renewalCredentialPattern = /^signet_renew_[A-Za-z0-9_-]{43}$/;
7
7
  export const nonceSchema = z.string().regex(token43);
8
- export const deviceCodeSchema = z.string().regex(token43);
9
8
 
10
9
  export const archiveDocumentIdSchema = z.string().regex(/^[a-f0-9]{64}$/);
11
10
  export const archiveFieldSchema = z.enum(['id', 'title', 'content', 'tags', 'createdAt']);
@@ -94,58 +93,93 @@ const signetTerms = {
94
93
  maxRequests: z.number().int().min(1).max(100000).nullable(),
95
94
  maxConcurrent: z.number().int().min(1).max(20),
96
95
  };
97
- /** Kinds of integration a device can pair as; approval creates one and issues it a Signet. */
98
- export const pairableProviderSchema = z.enum(['foundry', 'archive']);
99
- export type PairableProvider = z.infer<typeof pairableProviderSchema>;
96
+ /** A local Foundry, Archive or Oracle known to Kingdom by its key. */
97
+ export const installationKindSchema = z.enum(['foundry', 'archive', 'oracle']);
98
+ export type InstallationKind = z.infer<typeof installationKindSchema>;
100
99
  export const signetProposalSchema = z.strictObject({
101
100
  ...signetTerms,
102
101
  integrationId: z.uuid(),
103
102
  resources: z.array(signetResourceSchema).min(1).max(100),
104
103
  });
105
- /** A local Archive names its sourceId; Kingdom binds every write through its Signets to it. */
106
- const deviceIdentity = (value: { provider?: PairableProvider; deviceId?: string }) =>
107
- value.provider !== 'archive' || z.uuid().safeParse(value.deviceId).success;
108
- const deviceIdentityMessage = { message: 'A local Archive pairs with its sourceId as deviceId' };
109
- export const integrationPairingProposalSchema = z
110
- .strictObject({
111
- ...signetTerms,
112
- provider: pairableProviderSchema,
113
- deviceId: z.string().min(1).max(256).optional(),
114
- resources: z.array(signetResourceSchema).max(100),
115
- lifecycle: z.literal('ongoing'),
116
- })
117
- .refine(deviceIdentity, deviceIdentityMessage);
118
- export const signetRequestSchema = z
119
- .strictObject({
120
- ...signetTerms,
121
- integrationId: z.uuid().optional(),
122
- provider: pairableProviderSchema.optional(),
123
- deviceId: z.string().min(1).max(256).optional(),
124
- resources: z.array(signetResourceSchema).max(100),
125
- publicKey: publicClientKeySchema,
126
- })
127
- .refine((value) => Boolean(value.integrationId) !== Boolean(value.provider), {
128
- message: 'Name the host integration, or the kind of integration to pair',
129
- })
130
- .refine((value) => Boolean(value.provider) || !value.deviceId, {
131
- message: 'Only integration pairing names a device',
132
- })
133
- .refine(deviceIdentity, deviceIdentityMessage);
104
+ export const signetReviewProposalSchema = z.strictObject({
105
+ ...signetTerms,
106
+ resources: z.array(signetResourceSchema).max(100),
107
+ });
108
+ /** What an Installation asks for when it asks to be registered as an owner's integration. */
109
+ export const registrationTermsSchema = z.strictObject({
110
+ ...signetTerms,
111
+ lifecycle: z.literal('ongoing'),
112
+ resources: z.array(signetResourceSchema).max(100),
113
+ });
114
+ /** A person asks for a Signet on an integration, to be presented with `publicKey`. */
115
+ export const signetRequestSchema = z.strictObject({
116
+ ...signetTerms,
117
+ integrationId: z.uuid(),
118
+ resources: z.array(signetResourceSchema).min(1).max(100),
119
+ publicKey: publicClientKeySchema,
120
+ });
134
121
  export const signetRequestResponseSchema = z.object({
135
122
  requestId: z.uuid(),
136
123
  reviewCode: z.string(),
137
- deviceCode: deviceCodeSchema,
138
124
  expiresAt: z.iso.datetime(),
139
125
  });
140
- export const signetReviewProposalSchema = z.strictObject({
141
- ...signetTerms,
142
- resources: z.array(signetResourceSchema).max(100),
143
- });
144
126
  export type SignetProposal = z.infer<typeof signetProposalSchema>;
145
- export type IntegrationPairingProposal = z.infer<typeof integrationPairingProposalSchema>;
127
+ export type RegistrationTerms = z.infer<typeof registrationTermsSchema>;
146
128
  export type SignetRequest = z.infer<typeof signetRequestSchema>;
147
129
  export type SignetRequestResponse = z.infer<typeof signetRequestResponseSchema>;
148
130
 
131
+ /** A local Archive names its sourceId; Kingdom binds every write through its Signets to it. */
132
+ export const registerInstallationSchema = z
133
+ .strictObject({
134
+ kind: installationKindSchema,
135
+ name: z.string().trim().min(1).max(120),
136
+ sourceId: z.uuid().optional(),
137
+ publicKey: publicClientKeySchema,
138
+ })
139
+ .refine((value) => value.kind !== 'archive' || Boolean(value.sourceId), {
140
+ message: 'A local Archive registers with its sourceId',
141
+ });
142
+ export const registerInstallationResponseSchema = z.object({ installationId: z.uuid() });
143
+ export const requestRegistrationSchema = z.strictObject({ terms: registrationTermsSchema });
144
+ export const requestRegistrationResponseSchema = z.object({
145
+ reviewCode: z.string(),
146
+ expiresAt: z.iso.datetime(),
147
+ });
148
+ export const installationInquirySchema = z.object({
149
+ id: z.uuid(),
150
+ type: z.string(),
151
+ status: z.enum(['draft', 'sent', 'changesRequested', 'approved', 'denied', 'canceled']),
152
+ createdAt: z.iso.datetime(),
153
+ expiresAt: z.iso.datetime().nullable(),
154
+ owner: ownerRefSchema.nullable(),
155
+ ownerName: z.string().nullable(),
156
+ integrationId: z.uuid().nullable(),
157
+ signetId: z.uuid().nullable(),
158
+ deliverBefore: z.iso.datetime().nullable(),
159
+ });
160
+ export const installationInquiriesResponseSchema = z.object({
161
+ pending: z.object({ reviewCode: z.string(), expiresAt: z.iso.datetime() }).nullable(),
162
+ declinedAt: z.iso.datetime().nullable(),
163
+ inquiries: z.array(installationInquirySchema),
164
+ });
165
+ export const inquiryReferenceSchema = z.strictObject({ inquiryId: z.uuid() });
166
+ export const installationSignetsResponseSchema = z.object({
167
+ signets: z.array(
168
+ z.object({
169
+ signetId: z.uuid(),
170
+ integrationId: z.uuid(),
171
+ name: z.string(),
172
+ owner: ownerRefSchema,
173
+ }),
174
+ ),
175
+ });
176
+ export const enrollInstallationSignetSchema = z.strictObject({ signetId: z.uuid() });
177
+ export const rotateInstallationKeySchema = z.strictObject({
178
+ publicKey: publicClientKeySchema,
179
+ proof: z.string().max(8192),
180
+ });
181
+ export type InstallationInquiry = z.infer<typeof installationInquirySchema>;
182
+
149
183
  export const signetDescriptionSchema = z.object({
150
184
  signetId: z.uuid(),
151
185
  integrationId: z.uuid(),