@registrystack/client 0.36.0 → 0.38.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
@@ -1,7 +1,7 @@
1
1
  # @registrystack/client
2
2
 
3
3
  One versioned Node.js package for the Discovery, Evidence, Relay, Base Registry
4
- Engine, and Casework client APIs in Registry Stack.
4
+ Engine, Casework, and Messaging client APIs in Registry Stack.
5
5
 
6
6
  ## Install
7
7
 
@@ -11,13 +11,13 @@ npm install "@registrystack/client@<version>"
11
11
 
12
12
  Requires Node.js 22.12 or newer. Supported targets are macOS arm64, Linux
13
13
  arm64 with glibc, and Linux x64 with glibc; installing the package pulls in
14
- one platform-specific optional dependency containing all five native
14
+ one platform-specific optional dependency containing all six native
15
15
  bindings.
16
16
 
17
17
  ## Usage
18
18
 
19
19
  ```js
20
- const { discovery, evidence, relay, breg, casework } = require('@registrystack/client');
20
+ const { discovery, evidence, relay, breg, casework, messaging } = require('@registrystack/client');
21
21
 
22
22
  const registry = new breg.BaseRegistryClient({
23
23
  baseUrl: 'https://registry.example.invalid/',
@@ -25,6 +25,9 @@ const registry = new breg.BaseRegistryClient({
25
25
  const work = new casework.CaseworkClient({
26
26
  baseUrl: 'https://casework.example.invalid/',
27
27
  });
28
+ const messages = new messaging.MessagingClient({
29
+ baseUrl: 'https://messaging.example.invalid/',
30
+ });
28
31
  ```
29
32
 
30
33
  ## Products
@@ -38,6 +41,8 @@ const work = new casework.CaseworkClient({
38
41
  applied-request result navigation.
39
42
  - `casework`: Registry Casework, staff inbox, claims, drafts, decisions,
40
43
  recovery, history, holdings, and directory bootstrap.
44
+ - `messaging`: Registry Messaging, submit, inspect, cancel, and preview
45
+ institution-owned messages.
41
46
 
42
47
  Each product remains in its own namespace because its routing,
43
48
  authentication, errors, and verification rules are different.
@@ -99,6 +104,9 @@ is active.
99
104
  The `casework` namespace is part of the unified package beginning with
100
105
  Registry Stack v0.30.0.
101
106
 
107
+ The `messaging` namespace is part of the unified package beginning with
108
+ Registry Stack v0.38.0.
109
+
102
110
  ## Casework notes
103
111
 
104
112
  The Casework module is for a trusted server host. Each call takes that request's
@@ -134,12 +134,12 @@ export interface Draft {
134
134
  export interface DraftResponse { draft: Draft }
135
135
  export interface DecideRequest {
136
136
  displayedBinding: SourceBinding
137
- sourceProfileId: string
138
137
  operation: OperationName
139
138
  reason?: string
140
139
  flaggedFields?: ReadonlyArray<string>
141
140
  }
142
- export interface RecoverAttemptRequest { sourceProfileId: string }
141
+ /** The Registry-Source-Profile header selects the source profile; the body is empty. */
142
+ export type RecoverAttemptRequest = Record<string, never>
143
143
  export interface SourceReceipt {
144
144
  sourceRevision: string
145
145
  resultingState: string
@@ -170,9 +170,24 @@ export interface AttemptReservedHistoryEntry extends HistoryEntryBase {
170
170
  kind: 'attempt_reserved'
171
171
  detail: JsonObject & { attemptId: string; bindingReference: string; operation: OperationName }
172
172
  }
173
+ /**
174
+ * An attempt_uncertain detail takes one of three shapes: recovery left the
175
+ * attempt uncertain, the source definitively refused it, or an operator marked
176
+ * an expired pending attempt uncertain. reason is the original decision reason.
177
+ */
178
+ export type AttemptUncertainHistoryDetail = JsonObject & {
179
+ attemptId: string
180
+ bindingReference: string
181
+ operation: OperationName
182
+ reason?: string
183
+ } & (
184
+ | { sourceRevision?: string | null; definitivelyRefused?: never; operatorReason?: never }
185
+ | { definitivelyRefused: true; operatorReason?: never }
186
+ | { operatorReason: string; decidedBy: string; originalActor: IssuerPrincipal; originalProfileId: string; definitivelyRefused?: never }
187
+ )
173
188
  export interface AttemptUncertainHistoryEntry extends HistoryEntryBase {
174
189
  kind: 'attempt_uncertain'
175
- detail: JsonObject & { attemptId: string; bindingReference: string; sourceRevision?: string | null; definitivelyRefused?: boolean }
190
+ detail: AttemptUncertainHistoryDetail
176
191
  }
177
192
  export interface ActionCompletedHistoryEntry extends HistoryEntryBase {
178
193
  kind: 'action_completed'
@@ -271,6 +286,7 @@ export type KnownCaseworkProblemCode =
271
286
  | 'profile.not-human'
272
287
  | 'request.body-too-large'
273
288
  | 'request.invalid'
289
+ | 'request.limit-out-of-range'
274
290
  | 'request.method-not-allowed'
275
291
  | 'request.not-found'
276
292
  | 'request.reason-unsupported'
@@ -407,7 +423,7 @@ export interface ReviewerTask {
407
423
  state: ReviewerTaskState
408
424
  decidedByCaller?: boolean
409
425
  }
410
- export interface ReviewTaskPage { items: ReadonlyArray<ReviewerTask>; nextCursor?: Uuid }
426
+ export interface ReviewTaskPage { items: ReadonlyArray<ReviewerTask>; nextCursor?: Uuid; status: PageStatus }
411
427
  export type ReviewSourceBindingStatus = 'current' | 'binding_changed'
412
428
  export interface ReviewSourceProjection { binding: SourceBinding; displayReference?: string; display: Readonly<Record<string, JsonValue>> }
413
429
  export type ReviewTaskContextData =
@@ -1,8 +1,9 @@
1
- // The package's actual entry point. `index.d.ts` is auto-generated by
2
- // `napi build --platform` on every build and is what `check:types` diffs
3
- // byte-for-byte; the hand-written surface below is checked separately, by
4
- // `__test__/drift.test.js`, against the real module. Keep this list explicit:
5
- // generated loader diagnostics are not exported by client.js.
1
+ // The package's actual entry point. `index.d.ts` is napi-rs output that
2
+ // `package.json`'s `build` script rewrites and `check:types` diffs
3
+ // byte-for-byte against a fresh generation; the hand-written surface below is
4
+ // checked separately, by `__test__/drift.test.js`, against the real module.
5
+ // Keep this list explicit: generated loader diagnostics are not exported by
6
+ // client.js.
6
7
  export {
7
8
  AudienceScopedResult,
8
9
  PreparedEvidenceRequest,
@@ -1,9 +1,10 @@
1
1
  'use strict';
2
2
 
3
- // The package's actual entry point. `index.js`/`index.d.ts` are regenerated
4
- // by `napi build --platform` on every build (see `package.json`'s `build`
5
- // and `build:debug` scripts) and stay untouched here; this file sits on top
6
- // of them so a hand edit never gets silently overwritten by the next build.
3
+ // The package's actual entry point. `index.js`/`index.d.ts` are napi-rs
4
+ // output: `package.json`'s `build` script rewrites them in place,
5
+ // `check:types` requires a fresh generation to match them byte for byte, and
6
+ // `build:debug` leaves them untouched. This file sits on top of them so a hand
7
+ // edit never gets silently overwritten by the next build.
7
8
 
8
9
  const native = require('./index');
9
10
 
package/index.d.ts CHANGED
@@ -3,5 +3,6 @@ import * as evidence from './evidence/client'
3
3
  import * as relay from './relay/client'
4
4
  import * as breg from './breg/client'
5
5
  import * as casework from './casework/client'
6
+ import * as messaging from './messaging/client'
6
7
 
7
- export { discovery, evidence, relay, breg, casework }
8
+ export { discovery, evidence, relay, breg, casework, messaging }
package/index.js CHANGED
@@ -2,9 +2,10 @@
2
2
 
3
3
  // One assignment per namespace, never a `module.exports` object literal: Node
4
4
  // scans this file for these statements to synthesise the named exports an ESM
5
- // consumer imports, and `require()` sees the same five properties either way.
5
+ // consumer imports, and `require()` sees the same six properties either way.
6
6
  exports.discovery = require('./discovery/client');
7
7
  exports.evidence = require('./evidence/client');
8
8
  exports.relay = require('./relay/client');
9
9
  exports.breg = require('./breg/client');
10
10
  exports.casework = require('./casework/client');
11
+ exports.messaging = require('./messaging/client');
@@ -0,0 +1,143 @@
1
+ /** An integer for which `Number.isSafeInteger` is true. */
2
+ export type SafeInteger = number
3
+ /** A lowercase hyphenated UUID, checked by the native binding before any request is sent. */
4
+ export type MessageId = string
5
+ /** An RFC 3339 instant, parsed by the runtime. */
6
+ export type Instant = string
7
+ export type JsonScalar = string | number | boolean | null
8
+ export type JsonValue = JsonScalar | ReadonlyArray<JsonValue> | { readonly [key: string]: JsonValue }
9
+
10
+ export interface MessagingClientConfig {
11
+ baseUrl: string
12
+ requestTimeoutMilliseconds?: SafeInteger | null
13
+ connectTimeoutMilliseconds?: SafeInteger | null
14
+ maxResponseBytes?: SafeInteger | null
15
+ userAgent?: string | null
16
+ trustedRootCertificates?: string | null
17
+ }
18
+
19
+ export type Channel = 'email' | 'sms'
20
+ export type MessageStatus = 'queued' | 'sending' | 'submitted' | 'delivered' | 'failed' | 'expired' | 'cancelled' | 'unknown'
21
+ export type MessageDispatch = 'queued' | 'sending' | 'submitted' | 'failed' | 'unknown' | 'cancelled' | 'expired'
22
+ export type MessageReport = 'none' | 'sent' | 'delivered' | 'undelivered' | 'unavailable'
23
+ export type AttemptOutcome = 'in-progress' | 'accepted' | 'transient' | 'permanent' | 'maybe-sent' | 'interrupted'
24
+
25
+ /** Exactly one email address or one E.164 phone number. */
26
+ export type Recipient = { email: string; phone?: never } | { phone: string; email?: never }
27
+ export interface TemplateReference { id: string; version: string }
28
+ export interface DirectContent { subject?: string; text: string }
29
+
30
+ /** Exactly one of `template` or `content`; `locale` and `data` accompany a template. */
31
+ export interface SubmitMessageRequest {
32
+ senderProfile: string
33
+ to: Recipient
34
+ template?: TemplateReference
35
+ locale?: string
36
+ data?: JsonValue
37
+ content?: DirectContent
38
+ notBefore?: Instant
39
+ expiresAt?: Instant
40
+ correlationId?: string
41
+ }
42
+
43
+ export interface MessageLinks { self: string; cancel: string }
44
+ export interface MessageReceipt { id: MessageId; status: MessageStatus; links: MessageLinks }
45
+ export interface AttemptSummary {
46
+ generation: SafeInteger
47
+ attempt: SafeInteger
48
+ outcome: AttemptOutcome
49
+ startedAt: Instant
50
+ finishedAt?: Instant
51
+ providerReference: boolean
52
+ }
53
+ export interface MessageView {
54
+ id: MessageId
55
+ status: MessageStatus
56
+ dispatch: MessageDispatch
57
+ report: MessageReport
58
+ reportedAt?: Instant
59
+ channel: Channel
60
+ senderProfile: string
61
+ to: Recipient
62
+ template?: TemplateReference
63
+ correlationId?: string
64
+ acceptedAt: Instant
65
+ notBefore?: Instant
66
+ expiresAt: Instant
67
+ updatedAt: Instant
68
+ attempts: ReadonlyArray<AttemptSummary>
69
+ links: MessageLinks
70
+ }
71
+
72
+ export interface TemplatePreviewRequest { locale: string; data: JsonValue }
73
+ export type SmsEncoding = 'gsm7' | 'ucs2'
74
+ /** Septets for GSM-7, UTF-16 code units for UCS-2. */
75
+ export interface SegmentCount { encoding: SmsEncoding; units: SafeInteger; segments: SafeInteger }
76
+ export interface RenderedParts { subject?: string; text: string; html?: string }
77
+ export interface TemplatePreview {
78
+ template: TemplateReference
79
+ locale: string
80
+ channel: Channel
81
+ packageDigest: string
82
+ parts: RenderedParts
83
+ sms?: SegmentCount
84
+ }
85
+
86
+ export interface MessagingOutcome<T> { kind: 'complete'; value: T; traceId: string }
87
+
88
+ /** The closed catalogue: the client answers any other code as a protocol failure. */
89
+ export type MessagingProblemCode =
90
+ | 'authentication.refused'
91
+ | 'callback.unreadable'
92
+ | 'callback.unverified'
93
+ | 'content.invalid'
94
+ | 'content.too-large'
95
+ | 'content.too-many-segments'
96
+ | 'idempotency.expired'
97
+ | 'idempotency.key-reused'
98
+ | 'message.dispatch-started'
99
+ | 'message.not-visible'
100
+ | 'message.terminal'
101
+ | 'operation.not-authorized'
102
+ | 'profile.not-authorized'
103
+ | 'quota.exceeded'
104
+ | 'rate-limit.exceeded'
105
+ | 'request.body-too-large'
106
+ | 'request.invalid'
107
+ | 'request.method-not-allowed'
108
+ | 'request.not-found'
109
+ | 'request.unprocessable'
110
+ | 'request.unsupported-media-type'
111
+ | 'service.unavailable'
112
+ | 'template.data-invalid'
113
+ | 'template.locale-unavailable'
114
+ | 'template.not-found'
115
+ | 'template.render-refused'
116
+ export type MessagingProtocolFailure = 'header_bounds' | 'trace_context' | 'media_type' | 'body' | 'problem' | 'status' | 'protocol'
117
+
118
+ export class MessagingClientError extends Error {
119
+ readonly kind: 'configuration' | 'invalid_request' | 'transport' | 'problem' | 'protocol'
120
+ readonly code?: MessagingProblemCode
121
+ readonly title?: string
122
+ readonly detail?: string
123
+ readonly status?: SafeInteger
124
+ readonly traceId?: string
125
+ /**
126
+ * Whole seconds a 429 limit refusal asked the caller to wait, from
127
+ * `Retry-After`, at most one day. Absent on every other failure; the
128
+ * client never waits or retries itself.
129
+ */
130
+ readonly retryAfterSeconds?: SafeInteger
131
+ readonly transportKind?: string
132
+ readonly protocolFailure?: MessagingProtocolFailure
133
+ }
134
+
135
+ export class MessagingClient {
136
+ constructor(config: MessagingClientConfig)
137
+ health(): Promise<MessagingOutcome<null>>
138
+ ready(): Promise<MessagingOutcome<null>>
139
+ submit(token: string, idempotencyKey: string, request: SubmitMessageRequest): Promise<MessagingOutcome<MessageReceipt>>
140
+ message(token: string, messageId: MessageId): Promise<MessagingOutcome<MessageView>>
141
+ cancel(token: string, messageId: MessageId): Promise<MessagingOutcome<MessageView>>
142
+ preview(token: string, templateId: string, version: string, request: TemplatePreviewRequest): Promise<MessagingOutcome<TemplatePreview>>
143
+ }
@@ -0,0 +1,121 @@
1
+ 'use strict';
2
+
3
+ const { types: { isProxy } } = require('node:util');
4
+ const native = require('./index');
5
+
6
+ const MAX_JSON_DEPTH = 64;
7
+ const MAX_JSON_NODES = 20_000;
8
+ const MAX_JSON_STRING_BYTES = 1024 * 1024;
9
+
10
+ class MessagingClientError extends Error {
11
+ constructor(envelope) {
12
+ super(envelope.message);
13
+ this.name = 'MessagingClientError';
14
+ this.kind = envelope.kind;
15
+ for (const field of ['code', 'title', 'detail', 'status', 'traceId', 'retryAfterSeconds', 'transportKind', 'protocolFailure']) {
16
+ if (envelope[field] !== undefined && envelope[field] !== null) this[field] = envelope[field];
17
+ }
18
+ }
19
+ }
20
+
21
+ function normalized(error, fallbackKind) {
22
+ if (error instanceof MessagingClientError) return error;
23
+ if (error instanceof Error && typeof error.message === 'string') {
24
+ try {
25
+ const envelope = JSON.parse(error.message);
26
+ if (envelope && typeof envelope.kind === 'string') return new MessagingClientError(envelope);
27
+ } catch {
28
+ // napi argument conversion errors have no mapped envelope.
29
+ }
30
+ }
31
+ if (fallbackKind) {
32
+ return new MessagingClientError({
33
+ kind: fallbackKind,
34
+ message: fallbackKind === 'configuration'
35
+ ? 'Messaging client configuration is invalid'
36
+ : 'Messaging client arguments are invalid',
37
+ });
38
+ }
39
+ return error;
40
+ }
41
+
42
+ function cloneJson(value, budget, depth) {
43
+ if (depth > MAX_JSON_DEPTH || ++budget.nodes > MAX_JSON_NODES) throw normalized({}, 'invalid_request');
44
+ if (value === null || typeof value === 'boolean') return value;
45
+ if (typeof value === 'string') {
46
+ budget.bytes += Buffer.byteLength(value, 'utf8');
47
+ if (budget.bytes > MAX_JSON_STRING_BYTES) throw normalized({}, 'invalid_request');
48
+ return value;
49
+ }
50
+ if (typeof value === 'number') {
51
+ if (!Number.isFinite(value) || (Number.isInteger(value) && !Number.isSafeInteger(value))) {
52
+ throw normalized({}, 'invalid_request');
53
+ }
54
+ return value;
55
+ }
56
+ if (value === undefined || typeof value !== 'object' || isProxy(value)) {
57
+ throw normalized({}, 'invalid_request');
58
+ }
59
+ const array = Array.isArray(value);
60
+ const prototype = Object.getPrototypeOf(value);
61
+ if (array ? prototype !== Array.prototype : prototype !== Object.prototype && prototype !== null) {
62
+ throw normalized({}, 'invalid_request');
63
+ }
64
+ if (budget.active.has(value)) throw normalized({}, 'invalid_request');
65
+ budget.active.add(value);
66
+ try {
67
+ if (array) return value.map((member) => cloneJson(member, budget, depth + 1));
68
+ const result = Object.create(null);
69
+ for (const key of Reflect.ownKeys(value)) {
70
+ if (typeof key !== 'string') throw normalized({}, 'invalid_request');
71
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
72
+ if (!descriptor || !Object.hasOwn(descriptor, 'value')) throw normalized({}, 'invalid_request');
73
+ if (descriptor.enumerable) {
74
+ budget.bytes += Buffer.byteLength(key, 'utf8');
75
+ if (budget.bytes > MAX_JSON_STRING_BYTES) throw normalized({}, 'invalid_request');
76
+ result[key] = cloneJson(descriptor.value, budget, depth + 1);
77
+ }
78
+ }
79
+ return result;
80
+ } finally {
81
+ budget.active.delete(value);
82
+ }
83
+ }
84
+
85
+ function sanitize(value) {
86
+ return cloneJson(value, { nodes: 0, bytes: 0, active: new WeakSet() }, 0);
87
+ }
88
+
89
+ const NativeMessagingClient = native.MessagingClient;
90
+
91
+ class MessagingClient {
92
+ constructor(config) {
93
+ try {
94
+ this.native = new NativeMessagingClient(sanitize(config));
95
+ } catch (error) {
96
+ throw normalized(error, 'configuration');
97
+ }
98
+ }
99
+ }
100
+
101
+ for (const [method, jsonIndexes] of [
102
+ ['health', []],
103
+ ['ready', []],
104
+ ['submit', [2]],
105
+ ['message', []],
106
+ ['cancel', []],
107
+ ['preview', [3]],
108
+ ]) {
109
+ MessagingClient.prototype[method] = function (...args) {
110
+ try {
111
+ for (const index of jsonIndexes) {
112
+ if (args[index] !== undefined && args[index] !== null) args[index] = sanitize(args[index]);
113
+ }
114
+ return this.native[method](...args).catch((error) => { throw normalized(error); });
115
+ } catch (error) {
116
+ throw normalized(error, 'invalid_request');
117
+ }
118
+ };
119
+ }
120
+
121
+ module.exports = { MessagingClient, MessagingClientError };
@@ -0,0 +1,27 @@
1
+ /* auto-generated by NAPI-RS */
2
+ /* eslint-disable */
3
+
4
+ export declare class MessagingClient {
5
+ constructor(config: MessagingClientConfig)
6
+ health(): Promise<MessagingOutcome>
7
+ ready(): Promise<MessagingOutcome>
8
+ submit(token: string, idempotencyKey: string, request: any): Promise<MessagingOutcome>
9
+ message(token: string, messageId: string): Promise<MessagingOutcome>
10
+ cancel(token: string, messageId: string): Promise<MessagingOutcome>
11
+ preview(token: string, templateId: string, version: string, request: any): Promise<MessagingOutcome>
12
+ }
13
+
14
+ export interface MessagingClientConfig {
15
+ baseUrl: string
16
+ requestTimeoutMilliseconds?: number
17
+ connectTimeoutMilliseconds?: number
18
+ maxResponseBytes?: number
19
+ userAgent?: string
20
+ trustedRootCertificates?: string
21
+ }
22
+
23
+ export interface MessagingOutcome {
24
+ kind: string
25
+ value: any
26
+ traceId: string
27
+ }
@@ -0,0 +1,3 @@
1
+ 'use strict';
2
+
3
+ module.exports = require('../native').load('messaging');
package/native.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const PACKAGE_VERSION = '0.36.0';
4
- const PRODUCTS = new Set(['discovery', 'evidence', 'relay', 'breg', 'casework']);
3
+ const PACKAGE_VERSION = '0.38.0';
4
+ const PRODUCTS = new Set(['discovery', 'evidence', 'relay', 'breg', 'casework', 'messaging']);
5
5
 
6
6
  function target() {
7
7
  if (process.platform === 'darwin' && process.arch === 'arm64') return 'darwin-arm64';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@registrystack/client",
3
- "version": "0.36.0",
3
+ "version": "0.38.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/",
@@ -14,7 +14,8 @@
14
14
  "evidence",
15
15
  "relay",
16
16
  "breg",
17
- "casework"
17
+ "casework",
18
+ "messaging"
18
19
  ],
19
20
  "publishConfig": {
20
21
  "access": "public",
@@ -41,7 +42,8 @@
41
42
  "evidence",
42
43
  "relay",
43
44
  "breg",
44
- "casework"
45
+ "casework",
46
+ "messaging"
45
47
  ],
46
48
  "engines": {
47
49
  "node": ">=22.12.0"
@@ -57,8 +59,8 @@
57
59
  "typescript": "6.0.3"
58
60
  },
59
61
  "optionalDependencies": {
60
- "@registrystack/client-darwin-arm64": "0.36.0",
61
- "@registrystack/client-linux-arm64-gnu": "0.36.0",
62
- "@registrystack/client-linux-x64-gnu": "0.36.0"
62
+ "@registrystack/client-darwin-arm64": "0.38.0",
63
+ "@registrystack/client-linux-arm64-gnu": "0.38.0",
64
+ "@registrystack/client-linux-x64-gnu": "0.38.0"
63
65
  }
64
66
  }