@registrystack/client 0.30.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -79,6 +79,14 @@ mutation automatically. See
79
79
  [Exact JSON in Node](https://docs.registrystack.org/reference/client-api/#exact-json-in-node)
80
80
  for the full rules.
81
81
 
82
+ `breg.verifyWebhookDelivery({ method, path, headers, body, key })` authenticates
83
+ the exact bytes of one Version 1 webhook delivery. The result returns
84
+ `deliveryTime` and `idempotencyKey`; the receiver must bound clock skew and
85
+ deduplicate once-only effects on authenticated `source` plus `id`, or on an
86
+ application business key. See
87
+ [Verify a webhook delivery](https://docs.registrystack.org/reference/client-api/#verify-a-webhook-delivery)
88
+ for a receiver example.
89
+
82
90
  ## Versioning
83
91
 
84
92
  Install the exact client version that matches the deployment. The unified
package/breg/client.d.ts CHANGED
@@ -7,6 +7,41 @@ export type JsonObject = { readonly [key: string]: JsonValue }
7
7
  /** An integer that satisfies `Number.isSafeInteger` and the option's documented bounds. */
8
8
  export type SafeInteger = number
9
9
 
10
+ export type WebhookVerificationRefusalCode =
11
+ | 'missing_header'
12
+ | 'malformed_signature'
13
+ | 'signature_mismatch'
14
+ | 'unsupported_version'
15
+
16
+ export interface WebhookDeliveryInput {
17
+ method: string
18
+ path: string
19
+ headers: Readonly<Record<string, string>>
20
+ /** Exact bytes in a non-shared backing store. */
21
+ body: Buffer
22
+ /** Secret bytes in a non-shared backing store. */
23
+ key: Buffer
24
+ }
25
+
26
+ export interface VerifiedWebhookDelivery {
27
+ readonly id: string
28
+ readonly source: string
29
+ readonly type: string
30
+ readonly time: string
31
+ readonly dataschema: string
32
+ readonly generation: string
33
+ readonly attempt: string
34
+ readonly deliveryTime: string
35
+ readonly idempotencyKey: string
36
+ readonly body: Buffer
37
+ }
38
+
39
+ /**
40
+ * Authenticate one exact Base Registry Engine Version 1 webhook delivery.
41
+ * Apply delivery-time skew and idempotency policy before acting on the body.
42
+ */
43
+ export declare function verifyWebhookDelivery(input: WebhookDeliveryInput): VerifiedWebhookDelivery
44
+
10
45
  export interface PrivateJwk {
11
46
  readonly kty: string
12
47
  readonly kid: string
@@ -18,7 +53,23 @@ export interface PrivateKeyJwtConfig {
18
53
  tokenEndpoint: string
19
54
  clientId: string
20
55
  clientKey: PrivateJwk
56
+ /**
57
+ * The audience of the client assertion (who checks the client's
58
+ * authentication). Defaults to the token endpoint URL. This is not the
59
+ * `resource` of the token request.
60
+ */
21
61
  audience?: string | null
62
+ /**
63
+ * The RFC 8707 resource indicator the token is requested for: the resource
64
+ * server's registered identifier, not a URL to fetch.
65
+ */
66
+ resource?: string | null
67
+ /**
68
+ * The scopes requested for the token, sent as one space-delimited `scope`
69
+ * parameter. A requested scope may narrow the client's registered
70
+ * permission set; it can never widen it.
71
+ */
72
+ scopes?: ReadonlyArray<string> | null
22
73
  assertionLifetimeSeconds?: SafeInteger | null
23
74
  refreshMarginSeconds?: SafeInteger | null
24
75
  requestTimeoutMilliseconds?: SafeInteger | null
@@ -27,6 +78,15 @@ export interface PrivateKeyJwtConfig {
27
78
  trustedRootCertificates?: string | null
28
79
  }
29
80
 
81
+ /** OAuth credentials are returned to the caller. Keep them out of logs and persistence. */
82
+ export class PrivateKeyJwt {
83
+ constructor(config: PrivateKeyJwtConfig)
84
+ /** Fresh RFC 8693 exchange. Requires resource and scopes; never shares the service cache. */
85
+ exchange(subjectToken: string): Promise<string>
86
+ /** Client credentials with the shared provider's bounded service-token cache. */
87
+ bearerToken(): Promise<string>
88
+ }
89
+
30
90
  export type BaseRegistryAuthorization =
31
91
  | { static: string }
32
92
  | { privateKeyJwt: PrivateKeyJwtConfig }
package/breg/client.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { types: { isProxy } } = require('node:util');
3
+ const { types: { isProxy, isSharedArrayBuffer } } = require('node:util');
4
4
  const native = require('./index');
5
5
 
6
6
  const MAX_JSON_DEPTH = 128;
@@ -50,6 +50,64 @@ function inputError(kind) {
50
50
  });
51
51
  }
52
52
 
53
+ function plainDataObject(value, expectedFields) {
54
+ if (value === null || typeof value !== 'object' || Array.isArray(value) || isProxy(value)) {
55
+ throw inputError('invalid_request');
56
+ }
57
+ const prototype = Object.getPrototypeOf(value);
58
+ if (prototype !== Object.prototype && prototype !== null) throw inputError('invalid_request');
59
+ const keys = Reflect.ownKeys(value);
60
+ if (keys.some((key) => typeof key !== 'string')
61
+ || keys.length !== expectedFields.length
62
+ || keys.some((key) => !expectedFields.includes(key))) throw inputError('invalid_request');
63
+ const result = Object.create(null);
64
+ for (const field of expectedFields) {
65
+ const descriptor = Object.getOwnPropertyDescriptor(value, field);
66
+ if (!descriptor || !descriptor.enumerable || !Object.hasOwn(descriptor, 'value')) {
67
+ throw inputError('invalid_request');
68
+ }
69
+ result[field] = descriptor.value;
70
+ }
71
+ return result;
72
+ }
73
+
74
+ function webhookHeaders(value) {
75
+ if (value === null || typeof value !== 'object' || Array.isArray(value) || isProxy(value)) {
76
+ throw inputError('invalid_request');
77
+ }
78
+ const prototype = Object.getPrototypeOf(value);
79
+ if (prototype !== Object.prototype && prototype !== null) throw inputError('invalid_request');
80
+ const keys = Reflect.ownKeys(value);
81
+ if (keys.some((key) => typeof key !== 'string')) {
82
+ throw inputError('invalid_request');
83
+ }
84
+ const result = Object.create(null);
85
+ for (const name of keys) {
86
+ const descriptor = Object.getOwnPropertyDescriptor(value, name);
87
+ if (!descriptor || !descriptor.enumerable || !Object.hasOwn(descriptor, 'value')
88
+ || typeof descriptor.value !== 'string') throw inputError('invalid_request');
89
+ result[name] = descriptor.value;
90
+ }
91
+ return result;
92
+ }
93
+
94
+ function verifyWebhookDelivery(value) {
95
+ try {
96
+ const input = plainDataObject(value, ['method', 'path', 'headers', 'body', 'key']);
97
+ if (typeof input.method !== 'string' || typeof input.path !== 'string'
98
+ || !Buffer.isBuffer(input.body) || !Buffer.isBuffer(input.key)
99
+ || isSharedArrayBuffer(input.body.buffer) || isSharedArrayBuffer(input.key.buffer)) {
100
+ throw inputError('invalid_request');
101
+ }
102
+ input.headers = webhookHeaders(input.headers);
103
+ input.body = Buffer.from(input.body);
104
+ input.key = Buffer.from(input.key);
105
+ return native.verifyWebhookDelivery(input);
106
+ } catch (error) {
107
+ throw normalize(error, 'invalid_request');
108
+ }
109
+ }
110
+
53
111
  function chargeString(value, budget, kind) {
54
112
  budget.stringBytes += Buffer.byteLength(value, 'utf8');
55
113
  if (budget.stringBytes > MAX_JSON_STRING_BYTES) throw inputError(kind);
@@ -288,7 +346,25 @@ class BaseRegistryClient extends native.BaseRegistryClient {
288
346
  }
289
347
  }
290
348
 
349
+ class PrivateKeyJwt {
350
+ #native;
351
+ constructor(config) {
352
+ try { this.#native = new native.PrivateKeyJwt(cloneJson(config, { nodes: 0, stringBytes: 0, active: new WeakSet() }, 0, 'configuration')); }
353
+ catch (error) { throw normalize(error, 'configuration'); }
354
+ }
355
+ async exchange(subjectToken) {
356
+ try { return await this.#native.exchange(subjectToken); }
357
+ catch (error) { throw normalize(error, 'token'); }
358
+ }
359
+ async bearerToken() {
360
+ try { return await this.#native.bearerToken(); }
361
+ catch (error) { throw normalize(error, 'token'); }
362
+ }
363
+ }
364
+
291
365
  module.exports = {
366
+ verifyWebhookDelivery,
367
+ PrivateKeyJwt,
292
368
  BaseRegistryClient,
293
369
  BaseRegistryClientError,
294
370
  BRegMetadata: native.BRegMetadata,
package/breg/index.d.ts CHANGED
@@ -244,6 +244,14 @@ export declare class BRegTombstoneBinding {
244
244
  }
245
245
  export type TombstoneBinding = BRegTombstoneBinding
246
246
 
247
+ /** Shared OAuth client authentication and uncached task-token exchange. */
248
+ export declare class PrivateKeyJwt {
249
+ constructor(config: any)
250
+ exchange(subjectToken: string): Promise<string>
251
+ bearerToken(): Promise<string>
252
+ }
253
+ export type PrivateKeyJwtBinding = PrivateKeyJwt
254
+
247
255
  export interface CompleteOutcome {
248
256
  kind: string
249
257
  value: any
@@ -281,3 +289,29 @@ export interface RawOutcome {
281
289
  traceId: string
282
290
  etag?: string
283
291
  }
292
+
293
+ /** Authenticated BReg webhook metadata and exact body. */
294
+ export interface VerifiedWebhookDelivery {
295
+ id: string
296
+ source: string
297
+ type: string
298
+ time: string
299
+ dataschema: string
300
+ generation: string
301
+ attempt: string
302
+ deliveryTime: string
303
+ idempotencyKey: string
304
+ body: Buffer
305
+ }
306
+
307
+ /** Verify a BReg Version 1 webhook signature without applying receiver policy. */
308
+ export declare function verifyWebhookDelivery(input: WebhookDeliveryInput): VerifiedWebhookDelivery
309
+
310
+ /** Exact BReg webhook request supplied by a receiver. */
311
+ export interface WebhookDeliveryInput {
312
+ method: string
313
+ path: string
314
+ headers: Record<string, string>
315
+ body: Buffer
316
+ key: Buffer
317
+ }
@@ -21,6 +21,30 @@ export type PageStatus = 'complete' | 'budget_exhausted' | 'source_unavailable'
21
21
  export type HistoryKind = 'observed' | 'opened' | 'claimed' | 'assigned' | 'delegated' | 'caseload_moved' | 'clock_reminder' | 'clock_step_applied' | 'clock_recomputed' | 'released' | 'draft_saved' | 'attempt_reserved' | 'attempt_uncertain' | 'action_completed' | 'attempt_settled' | 'superseded' | 'completed'
22
22
 
23
23
  export interface IssuerPrincipal { issuer: string; subject: string }
24
+ export type TaskGrantBounds = { type: 'evidence'; requirement: string } | { type: 'breg'; permissions: ReadonlyArray<TaskPermission> }
25
+ export interface TaskPermission { collection: string; operations: ReadonlyArray<string> }
26
+ export interface EvidenceRequesterContext { requesterTags: ReadonlyArray<string>; audience: string }
27
+ export interface TaskApprovalRequest { templateId: string; templateVersion: string }
28
+ export interface TaskTemplatePreview {
29
+ id: string; version: string; label: string; agent: IssuerPrincipal; client: string; resource: string; scopes: ReadonlyArray<string>; purpose: string
30
+ evidenceContext?: EvidenceRequesterContext; bounds: TaskGrantBounds; subjects: { readonly [key: string]: Exclude<JsonScalar, null> }; lifetimeSeconds: SafeInteger
31
+ }
32
+ export interface TaskTemplatePreviews { itemRevision: SafeInteger; templates: ReadonlyArray<TaskTemplatePreview> }
33
+ /** Grant metadata deliberately excludes stored subject values. */
34
+ export interface TaskGrantView {
35
+ id: string; templateId: string; templateVersion: string; agent: IssuerPrincipal; client: string; resource: string; scopes: ReadonlyArray<string>; purpose: string
36
+ evidenceContext?: EvidenceRequesterContext; bounds: TaskGrantBounds; expiresAt: SafeInteger; invalidated: boolean
37
+ }
38
+ export interface TaskGrantList { grants: ReadonlyArray<TaskGrantView> }
39
+ export interface TaskGrantRevocation { id: string; invalidated: boolean }
40
+ /** A short-lived credential. Do not persist or log the assertion. */
41
+ export interface TaskAssertionResponse { assertion: string; expiresAt: SafeInteger; grantExpiresAt: SafeInteger }
42
+ export interface TaskGrantStatusDetails {
43
+ grantId: string; sourceIssuer: string; principal: string; client: string; resource: string; purpose: string
44
+ bounds: TaskGrantBounds; subjects: { readonly [key: string]: Exclude<JsonScalar, null> }; expiresAt: SafeInteger
45
+ }
46
+ export interface TaskGrantStatus { active: boolean; grant?: TaskGrantStatusDetails }
47
+
24
48
  export interface DirectoryMember { issuer: string; subject: string; displayName?: string | null }
25
49
  export interface SubjectRef { sourceId: string; kind: string; id: string }
26
50
  export interface SourceBinding {
@@ -326,12 +350,18 @@ export type KnownCaseworkProblemCode =
326
350
  | 'request.invalid'
327
351
  | 'request.method-not-allowed'
328
352
  | 'request.not-found'
353
+ | 'request.reason-unsupported'
354
+ | 'request.source-rejected'
329
355
  | 'request.unprocessable'
330
356
  | 'request.unsupported-media-type'
331
357
  | 'runtime.failure'
332
358
  | 'service.unavailable'
359
+ | 'source-profile.not-applicable'
360
+ | 'source-profile.required'
333
361
  | 'source.bad-gateway'
334
362
  | 'source.not-found'
363
+ | 'source.record-missing'
364
+ | 'source.reviewer-not-authorized'
335
365
  | 'source.signature-invalid'
336
366
  | 'work-item.already-claimed'
337
367
  | 'work-item.not-holder'
@@ -405,6 +435,12 @@ export class CaseworkClient {
405
435
  listWorkItems(token: string, profile: string, sourceProfile: string, query: ListWorkItemsQuery): Promise<CaseworkOutcome<WorkItemPage>>
406
436
  nextWorkItem(token: string, profile: string, sourceProfile: string, query?: NextWorkItemQuery | null): Promise<CaseworkOutcome<WorkItemPage>>
407
437
  getWorkItem(token: string, profile: string, sourceProfile: string, itemId: string): Promise<CaseworkOutcome<WorkItem>>
438
+ previewTaskTemplates(token: string, profile: string, sourceProfile: string, itemId: string): Promise<CaseworkOutcome<TaskTemplatePreviews>>
439
+ listTaskGrants(token: string, profile: string, sourceProfile: string, itemId: string): Promise<CaseworkOutcome<TaskGrantList>>
440
+ approveTaskGrant(token: string, profile: string, sourceProfile: string, itemId: string, expectedRevision: SafeInteger, idempotencyKey: string, approval: TaskApprovalRequest): Promise<CaseworkOutcome<TaskGrantView>>
441
+ revokeTaskGrant(token: string, profile: string, sourceProfile: string, itemId: string, grantId: string): Promise<CaseworkOutcome<TaskGrantRevocation>>
442
+ taskAssertion(token: string, grantId: string): Promise<CaseworkOutcome<TaskAssertionResponse>>
443
+ taskGrantStatus(token: string, grantId: string): Promise<CaseworkOutcome<TaskGrantStatus>>
408
444
  claimWorkItem(token: string, profile: string, sourceProfile: string, action: CaseworkAction, idempotencyKey: string): Promise<CaseworkOutcome<MutationResponse>>
409
445
  releaseWorkItem(token: string, profile: string, sourceProfile: string, action: CaseworkAction, idempotencyKey: string): Promise<CaseworkOutcome<MutationResponse>>
410
446
  getDraft(token: string, profile: string, sourceProfile: string, itemId: string): Promise<CaseworkOutcome<DraftResponse>>
@@ -112,6 +112,12 @@ for (const [method, jsonIndexes] of [
112
112
  ['listWorkItems', [3]],
113
113
  ['nextWorkItem', [3]],
114
114
  ['getWorkItem', []],
115
+ ['previewTaskTemplates', []],
116
+ ['listTaskGrants', []],
117
+ ['approveTaskGrant', [6]],
118
+ ['revokeTaskGrant', []],
119
+ ['taskAssertion', []],
120
+ ['taskGrantStatus', []],
115
121
  ['claimWorkItem', [3]],
116
122
  ['releaseWorkItem', [3]],
117
123
  ['getDraft', []],
@@ -19,6 +19,12 @@ export declare class CaseworkClient {
19
19
  listWorkItems(token: string, profile: string, sourceProfile: string, query: any): Promise<CaseworkOutcome>
20
20
  nextWorkItem(token: string, profile: string, sourceProfile: string, query?: any | undefined | null): Promise<CaseworkOutcome>
21
21
  getWorkItem(token: string, profile: string, sourceProfile: string, itemId: string): Promise<CaseworkOutcome>
22
+ previewTaskTemplates(token: string, profile: string, sourceProfile: string, itemId: string): Promise<CaseworkOutcome>
23
+ listTaskGrants(token: string, profile: string, sourceProfile: string, itemId: string): Promise<CaseworkOutcome>
24
+ approveTaskGrant(token: string, profile: string, sourceProfile: string, itemId: string, expectedRevision: number, idempotencyKey: string, approval: any): Promise<CaseworkOutcome>
25
+ revokeTaskGrant(token: string, profile: string, sourceProfile: string, itemId: string, grantId: string): Promise<CaseworkOutcome>
26
+ taskAssertion(token: string, grantId: string): Promise<CaseworkOutcome>
27
+ taskGrantStatus(token: string, grantId: string): Promise<CaseworkOutcome>
22
28
  claimWorkItem(token: string, profile: string, sourceProfile: string, action: any, idempotencyKey: string): Promise<CaseworkOutcome>
23
29
  releaseWorkItem(token: string, profile: string, sourceProfile: string, action: any, idempotencyKey: string): Promise<CaseworkOutcome>
24
30
  getDraft(token: string, profile: string, sourceProfile: string, itemId: string): Promise<CaseworkOutcome>
@@ -82,7 +82,23 @@ export interface PrivateKeyJwtConfig {
82
82
  tokenEndpoint: string
83
83
  clientId: string
84
84
  clientKey: Readonly<Record<string, unknown>>
85
+ /**
86
+ * The audience of the client assertion (who checks the client's
87
+ * authentication). Defaults to the token endpoint URL. This is not the
88
+ * `resource` of the token request.
89
+ */
85
90
  audience?: string
91
+ /**
92
+ * The RFC 8707 resource indicator the token is requested for: the resource
93
+ * server's registered identifier, not a URL to fetch.
94
+ */
95
+ resource?: string
96
+ /**
97
+ * The scopes requested for the token, sent as one space-delimited `scope`
98
+ * parameter. A requested scope may narrow the client's registered
99
+ * permission set; it can never widen it.
100
+ */
101
+ scopes?: ReadonlyArray<string>
86
102
  assertionLifetimeSeconds?: number
87
103
  refreshMarginSeconds?: number
88
104
  requestTimeoutMs?: number
package/native.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const PACKAGE_VERSION = '0.30.0';
3
+ const PACKAGE_VERSION = '0.31.0';
4
4
  const PRODUCTS = new Set(['discovery', 'evidence', 'relay', 'breg', 'casework']);
5
5
 
6
6
  function target() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@registrystack/client",
3
- "version": "0.30.0",
3
+ "version": "0.31.0",
4
4
  "description": "Unified Node.js client for Registry Stack products.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://docs.registrystack.org/reference/client-api/",
@@ -57,8 +57,8 @@
57
57
  "typescript": "6.0.3"
58
58
  },
59
59
  "optionalDependencies": {
60
- "@registrystack/client-darwin-arm64": "0.30.0",
61
- "@registrystack/client-linux-arm64-gnu": "0.30.0",
62
- "@registrystack/client-linux-x64-gnu": "0.30.0"
60
+ "@registrystack/client-darwin-arm64": "0.31.0",
61
+ "@registrystack/client-linux-arm64-gnu": "0.31.0",
62
+ "@registrystack/client-linux-x64-gnu": "0.31.0"
63
63
  }
64
64
  }
package/relay/client.d.ts CHANGED
@@ -14,7 +14,23 @@ export interface PrivateKeyJwtConfig {
14
14
  tokenEndpoint: string
15
15
  clientId: string
16
16
  clientKey: PrivateJwk
17
+ /**
18
+ * The audience of the client assertion (who checks the client's
19
+ * authentication). Defaults to the token endpoint URL. This is not the
20
+ * `resource` of the token request.
21
+ */
17
22
  audience?: string | null
23
+ /**
24
+ * The RFC 8707 resource indicator the token is requested for: the resource
25
+ * server's registered identifier, not a URL to fetch.
26
+ */
27
+ resource?: string | null
28
+ /**
29
+ * The scopes requested for the token, sent as one space-delimited `scope`
30
+ * parameter. A requested scope may narrow the client's registered
31
+ * permission set; it can never widen it.
32
+ */
33
+ scopes?: ReadonlyArray<string> | null
18
34
  assertionLifetimeSeconds?: SafeInteger | null
19
35
  refreshMarginSeconds?: SafeInteger | null
20
36
  requestTimeoutMilliseconds?: SafeInteger | null