@registrystack/client 0.30.0 → 0.32.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,9 +78,56 @@ 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
+
90
+ /** One immutable verified person or grant context. Reconstruct after host source facts change. */
91
+ export interface ExchangeContext {
92
+ issuer: string
93
+ subject: string
94
+ audience: string
95
+ generation: string
96
+ deadlineSeconds: SafeInteger
97
+ grantId?: string | null
98
+ }
99
+
100
+ export type ExchangePrivateKeyJwtConfig = PrivateKeyJwtConfig & {
101
+ resource: string
102
+ scopes: ReadonlyArray<string>
103
+ }
104
+
105
+ export interface FirstPartyExchangeSource {
106
+ key: PrivateJwk
107
+ /** Reviewed host attributes; registry_actor_kind must be human and grant claims are refused. */
108
+ attributes: Readonly<Record<string, JsonValue>>
109
+ }
110
+
111
+ export interface RemoteExchangeSource {
112
+ /** Exact assertion URL supplied by the authority client, such as Casework.taskAssertionEndpoint. */
113
+ endpoint: string
114
+ bootstrap: ExchangePrivateKeyJwtConfig
115
+ bootstrapResource: string
116
+ bootstrapScope: string
117
+ requestTimeoutMilliseconds?: SafeInteger | null
118
+ connectTimeoutMilliseconds?: SafeInteger | null
119
+ userAgent?: string | null
120
+ trustedRootCertificates?: string | null
121
+ }
122
+
123
+ export type ExchangeAuthorizationConfig =
124
+ | { client: ExchangePrivateKeyJwtConfig; context: ExchangeContext; firstParty: FirstPartyExchangeSource }
125
+ | { client: ExchangePrivateKeyJwtConfig; context: ExchangeContext; remote: RemoteExchangeSource }
126
+
30
127
  export type BaseRegistryAuthorization =
31
128
  | { static: string }
32
129
  | { privateKeyJwt: PrivateKeyJwtConfig }
130
+ | { exchange: ExchangeAuthorizationConfig }
33
131
 
34
132
  export interface BaseRegistryClientConfig {
35
133
  baseUrl: string
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,14 @@ 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
+ /** Exact empty-POST authority route for the shared remote exchange provider. */
444
+ taskAssertionEndpoint(grantId: string): string
445
+ taskGrantStatus(token: string, grantId: string): Promise<CaseworkOutcome<TaskGrantStatus>>
408
446
  claimWorkItem(token: string, profile: string, sourceProfile: string, action: CaseworkAction, idempotencyKey: string): Promise<CaseworkOutcome<MutationResponse>>
409
447
  releaseWorkItem(token: string, profile: string, sourceProfile: string, action: CaseworkAction, idempotencyKey: string): Promise<CaseworkOutcome<MutationResponse>>
410
448
  getDraft(token: string, profile: string, sourceProfile: string, itemId: string): Promise<CaseworkOutcome<DraftResponse>>
@@ -92,6 +92,14 @@ class CaseworkClient {
92
92
  throw normalized(error, 'configuration');
93
93
  }
94
94
  }
95
+
96
+ taskAssertionEndpoint(grantId) {
97
+ try {
98
+ return this.native.taskAssertionEndpoint(grantId);
99
+ } catch (error) {
100
+ throw normalized(error, 'invalid_request');
101
+ }
102
+ }
95
103
  }
96
104
 
97
105
  for (const [method, jsonIndexes] of [
@@ -112,6 +120,12 @@ for (const [method, jsonIndexes] of [
112
120
  ['listWorkItems', [3]],
113
121
  ['nextWorkItem', [3]],
114
122
  ['getWorkItem', []],
123
+ ['previewTaskTemplates', []],
124
+ ['listTaskGrants', []],
125
+ ['approveTaskGrant', [6]],
126
+ ['revokeTaskGrant', []],
127
+ ['taskAssertion', []],
128
+ ['taskGrantStatus', []],
115
129
  ['claimWorkItem', [3]],
116
130
  ['releaseWorkItem', [3]],
117
131
  ['getDraft', []],
@@ -19,6 +19,13 @@ 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
+ taskAssertionEndpoint(grantId: string): string
28
+ taskGrantStatus(token: string, grantId: string): Promise<CaseworkOutcome>
22
29
  claimWorkItem(token: string, profile: string, sourceProfile: string, action: any, idempotencyKey: string): Promise<CaseworkOutcome>
23
30
  releaseWorkItem(token: string, profile: string, sourceProfile: string, action: any, idempotencyKey: string): Promise<CaseworkOutcome>
24
31
  getDraft(token: string, profile: string, sourceProfile: string, itemId: string): Promise<CaseworkOutcome>
@@ -35,6 +35,8 @@ export interface ProgressiveVerifiedResult {
35
35
  readonly values: Readonly<Record<string, unknown>>
36
36
  readonly value: unknown
37
37
  readonly subjectContinuity: ProgressiveSubjectContinuity
38
+ /** Pre-response trust and policy snapshot. Retain with the exact artifact. */
39
+ readonly retainedVerification: Buffer
38
40
  }
39
41
 
40
42
  /** A signed-JWS result. The credential member of the native result is `null`. */
@@ -82,7 +84,23 @@ export interface PrivateKeyJwtConfig {
82
84
  tokenEndpoint: string
83
85
  clientId: string
84
86
  clientKey: Readonly<Record<string, unknown>>
87
+ /**
88
+ * The audience of the client assertion (who checks the client's
89
+ * authentication). Defaults to the token endpoint URL. This is not the
90
+ * `resource` of the token request.
91
+ */
85
92
  audience?: string
93
+ /**
94
+ * The RFC 8707 resource indicator the token is requested for: the resource
95
+ * server's registered identifier, not a URL to fetch.
96
+ */
97
+ resource?: string
98
+ /**
99
+ * The scopes requested for the token, sent as one space-delimited `scope`
100
+ * parameter. A requested scope may narrow the client's registered
101
+ * permission set; it can never widen it.
102
+ */
103
+ scopes?: ReadonlyArray<string>
86
104
  assertionLifetimeSeconds?: number
87
105
  refreshMarginSeconds?: number
88
106
  requestTimeoutMs?: number
@@ -100,12 +118,91 @@ export type EvidenceTokenConfig =
100
118
  | { static: string }
101
119
  | { privateKeyJwt: PrivateKeyJwtConfig }
102
120
 
121
+ /** A verified actor/grant generation with a bounded authorization lifetime. */
122
+ export interface EvidenceExchangeContext {
123
+ issuer: string
124
+ subject: string
125
+ audience: string
126
+ generation: string
127
+ deadlineSeconds: number
128
+ grantId?: string
129
+ }
130
+
131
+ /**
132
+ * The client half of an exchange authorization.
133
+ *
134
+ * This is not a `PrivateKeyJwtConfig`, even though the two describe the same
135
+ * kind of client: an exchange is pinned to one resource and scope set, so both
136
+ * are required here, and the object is read by the shared exchange
137
+ * configuration parser rather than by the binding's own reader, so its timeout
138
+ * members are the full-word ones that parser accepts. State at most one of
139
+ * `requestTimeoutMilliseconds` and `requestTimeoutSeconds`, and at most one of
140
+ * `connectTimeoutMilliseconds` and `connectTimeoutSeconds`; naming both of a
141
+ * pair is refused.
142
+ */
143
+ export interface EvidenceExchangeClientConfig {
144
+ tokenEndpoint: string
145
+ clientId: string
146
+ /** The client's private key, as a JWK. */
147
+ clientKey: Readonly<Record<string, unknown>>
148
+ /**
149
+ * The audience of the client assertion, when the authorization server
150
+ * expects something other than its own token endpoint.
151
+ */
152
+ audience?: string
153
+ /**
154
+ * The RFC 8707 resource indicator the token is requested for: the resource
155
+ * server's registered identifier, not a URL to fetch.
156
+ */
157
+ resource: string
158
+ /**
159
+ * The scopes requested for the token, sent as one space-delimited `scope`
160
+ * parameter. A requested scope may narrow the client's registered
161
+ * permission set; it can never widen it.
162
+ */
163
+ scopes: ReadonlyArray<string>
164
+ assertionLifetimeSeconds?: number
165
+ refreshMarginSeconds?: number
166
+ requestTimeoutMilliseconds?: number
167
+ requestTimeoutSeconds?: number
168
+ connectTimeoutMilliseconds?: number
169
+ connectTimeoutSeconds?: number
170
+ userAgent?: string
171
+ /** Additional trust roots for the token endpoint, as a PEM bundle. */
172
+ trustedRootCertificates?: string
173
+ }
174
+
175
+ /** The owning authority a remote-sourced exchange obtains assertions from. */
176
+ export interface EvidenceExchangeRemoteSourceConfig {
177
+ endpoint: string
178
+ bootstrap: EvidenceExchangeClientConfig
179
+ bootstrapResource: string
180
+ bootstrapScope: string
181
+ requestTimeoutMilliseconds?: number
182
+ requestTimeoutSeconds?: number
183
+ connectTimeoutMilliseconds?: number
184
+ connectTimeoutSeconds?: number
185
+ userAgent?: string
186
+ /** Additional trust roots for the authority, as a PEM bundle. */
187
+ trustedRootCertificates?: string
188
+ }
189
+
190
+ export type EvidenceExchangeAuthorizationConfig = {
191
+ client: EvidenceExchangeClientConfig
192
+ context: EvidenceExchangeContext
193
+ } & (
194
+ | { firstParty: { key: Readonly<Record<string, unknown>>, attributes: Readonly<Record<string, unknown>> }, remote?: never }
195
+ | { firstParty?: never, remote: EvidenceExchangeRemoteSourceConfig }
196
+ )
197
+
103
198
  /** The configuration `new EvidenceClient(...)` reads. */
104
199
  export interface EvidenceClientConfig {
105
200
  baseUrl: string
106
201
  trustedJwks: TrustedJwks
107
202
  revokedKeyIds: ReadonlyArray<string>
108
- token: EvidenceTokenConfig
203
+ /** Configure exactly one of `token` or `authorization`. */
204
+ token?: EvidenceTokenConfig
205
+ authorization?: { exchange: EvidenceExchangeAuthorizationConfig }
109
206
  requestTimeoutMs?: number
110
207
  connectTimeoutMs?: number
111
208
  userAgent?: string
@@ -136,6 +233,10 @@ export declare class EvidenceClient extends NativeEvidenceClient {
136
233
  * the JavaScript wrapper.
137
234
  */
138
235
  static fromProfile(path: string, privateKeyJwk?: Readonly<Record<string, unknown>>): EvidenceClient
236
+ static fromProfileWithAuthorization(
237
+ path: string,
238
+ authorization: { exchange: EvidenceExchangeAuthorizationConfig },
239
+ ): EvidenceClient
139
240
  }
140
241
 
141
242
  /**
@@ -166,6 +166,7 @@ for (const name of [
166
166
  'values',
167
167
  'value',
168
168
  'subjectContinuity',
169
+ 'retainedVerification',
169
170
  ]) {
170
171
  wrapGetter(native.AudienceScopedResult.prototype, name);
171
172
  }
@@ -215,6 +216,16 @@ class EvidenceClient extends native.EvidenceClient {
215
216
  throw normalize(error);
216
217
  }
217
218
  }
219
+
220
+ static fromProfileWithAuthorization(path, authorization) {
221
+ try {
222
+ const client = native.EvidenceClient.fromProfileWithAuthorization(path, authorization);
223
+ Object.setPrototypeOf(client, this.prototype);
224
+ return client;
225
+ } catch (error) {
226
+ throw normalize(error);
227
+ }
228
+ }
218
229
  }
219
230
 
220
231
  class SdJwtVcBatchResponse extends native.SdJwtVcBatchResponse {
@@ -236,4 +247,18 @@ module.exports = {
236
247
  RawEvidenceRequestBatchResponse: native.RawEvidenceRequestBatchResponse,
237
248
  AudienceScopedResult: native.AudienceScopedResult,
238
249
  SdJwtVcBatchResponse,
250
+ verifyRetained(context, response) {
251
+ try {
252
+ return native.verifyRetained(context, response);
253
+ } catch (error) {
254
+ throw normalize(error);
255
+ }
256
+ },
257
+ verifyRetainedAsOf(context, response, asOfMillis) {
258
+ try {
259
+ return native.verifyRetainedAsOf(context, response, asOfMillis);
260
+ } catch (error) {
261
+ throw normalize(error);
262
+ }
263
+ },
239
264
  };
@@ -6,6 +6,8 @@
6
6
  * FFI boundary until the caller asks for it.
7
7
  */
8
8
  export declare class AudienceScopedResult {
9
+ /** Exact serialized pre-response trust snapshot for offline re-verification. */
10
+ get retainedVerification(): Buffer
9
11
  get responseFormat(): string
10
12
  get evidence(): any
11
13
  get traceId(): string | null
@@ -36,6 +38,12 @@ export declare class EvidenceClient {
36
38
  * client and are likewise redacted there.
37
39
  */
38
40
  static fromProfile(path: string, privateKeyJwk?: any): EvidenceClient
41
+ /**
42
+ * Build a profile-driven client with a context-bound staff exchange.
43
+ * The profile's discovered authority and pinned OAuth request parameters
44
+ * must match the immutable provider before the first token is acquired.
45
+ */
46
+ static fromProfileWithAuthorization(path: string, authorization: any): EvidenceClient
39
47
  /**
40
48
  * Close the expectations for one request and generate its nonce. No I/O
41
49
  * happens here. The returned request is good for exactly one exchange:
@@ -300,3 +308,15 @@ export interface VerifiedEvidenceRequestBatch {
300
308
  export type VerifiedEvidenceRequestBatchItem =
301
309
  | { status: 'available', verified: VerifiedEvidence }
302
310
  | { status: 'notAvailable' }
311
+
312
+ /**
313
+ * Verify exact retained response bytes against a bounded pre-response trust
314
+ * snapshot without constructing a client or performing network I/O.
315
+ */
316
+ export declare function verifyRetained(context: Buffer, response: Buffer): VerifiedEvidence
317
+
318
+ /**
319
+ * Replay an earlier decision at its recorded instant. Current decisions use
320
+ * `verify_retained` so expired assertions cannot be accepted as current.
321
+ */
322
+ export declare function verifyRetainedAsOf(context: Buffer, response: Buffer, asOfMillis: number): VerifiedEvidence
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.32.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.32.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.32.0",
61
+ "@registrystack/client-linux-arm64-gnu": "0.32.0",
62
+ "@registrystack/client-linux-x64-gnu": "0.32.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
@@ -23,9 +39,45 @@ export interface PrivateKeyJwtConfig {
23
39
  trustedRootCertificates?: string | null
24
40
  }
25
41
 
42
+ /** One immutable verified person or grant context. Reconstruct after host source facts change. */
43
+ export interface ExchangeContext {
44
+ issuer: string
45
+ subject: string
46
+ audience: string
47
+ generation: string
48
+ deadlineSeconds: SafeInteger
49
+ grantId?: string | null
50
+ }
51
+
52
+ export type ExchangePrivateKeyJwtConfig = PrivateKeyJwtConfig & {
53
+ resource: string
54
+ scopes: ReadonlyArray<string>
55
+ }
56
+
57
+ export interface FirstPartyExchangeSource {
58
+ key: PrivateJwk
59
+ attributes: Readonly<Record<string, JsonValue>>
60
+ }
61
+
62
+ export interface RemoteExchangeSource {
63
+ endpoint: string
64
+ bootstrap: ExchangePrivateKeyJwtConfig
65
+ bootstrapResource: string
66
+ bootstrapScope: string
67
+ requestTimeoutMilliseconds?: SafeInteger | null
68
+ connectTimeoutMilliseconds?: SafeInteger | null
69
+ userAgent?: string | null
70
+ trustedRootCertificates?: string | null
71
+ }
72
+
73
+ export type ExchangeAuthorizationConfig =
74
+ | { client: ExchangePrivateKeyJwtConfig; context: ExchangeContext; firstParty: FirstPartyExchangeSource }
75
+ | { client: ExchangePrivateKeyJwtConfig; context: ExchangeContext; remote: RemoteExchangeSource }
76
+
26
77
  export type RelayAuthorization =
27
78
  | { static: string }
28
79
  | { privateKeyJwt: PrivateKeyJwtConfig }
80
+ | { exchange: ExchangeAuthorizationConfig }
29
81
 
30
82
  export interface RelayClientConfig {
31
83
  baseUrl: string