@substrat-run/connector-scrive 0.0.2

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.
@@ -0,0 +1,223 @@
1
+ import { type ConnectionId } from '@substrat-run/contracts';
2
+ import type { ConnectorHandler, ConnectorOptions, FetchLike, ScopeHost } from '@substrat-run/kernel';
3
+ export { ScriveApi, SCRIVE_TESTBED, SCRIVE_PRODUCTION } from './api.js';
4
+ export { ScriveMock } from './mock.js';
5
+ export { renderPdf } from './pdf.js';
6
+ /**
7
+ * The Scrive connector — the OUTBOUND half of external signing.
8
+ *
9
+ * `engine-protocol` emits `protocol.signatures-requested` when a vertical
10
+ * freezes a document and sends it for signature. This turns that into a Scrive
11
+ * document: create → set file → set parties (BankID) → start.
12
+ *
13
+ * ## The return path exists now (#97), but nothing schedules it yet
14
+ *
15
+ * The outbound half above is verified against the real testbed. The return path
16
+ * — recording a completed signature back onto the protocol instance — is
17
+ * `reconcileScriveDispatch` below. It could not be written until #97: a
18
+ * signature lives in the SCOPE database, `getScope` demands a `PrincipalId`, and
19
+ * a connector is not one. #97 gave a connection its own door
20
+ * (`getConnectorScope`) and made its authority an ordinary permission grant, so
21
+ * the driver records a signature by invoking `protocol/record-signature` as the
22
+ * connection itself.
23
+ *
24
+ * What each earlier gap became:
25
+ *
26
+ * 1. **Recording the provider's document id / dispatch idempotency.** Solved
27
+ * without #97 by a directory-side ledger (`ctx.admin.putConnectorState`): a
28
+ * redelivery finds the row and skips instead of sending a SECOND document.
29
+ * Directory-side because a connector runs INSIDE the scope's dispatch and
30
+ * re-entering the scope actor deadlocks. A narrow residual remains (ledger
31
+ * write failing after `start` succeeds) — closable with provider-side dedup
32
+ * via the `substrat_instance` tag.
33
+ * 2. **Recording a signature.** Solved by `reconcileScriveDispatch` on the #97
34
+ * seam — a top-level operation, OUTSIDE any dispatch, so re-entering the scope
35
+ * is safe. `sweepScriveReconciliations` is the poll driver over it: it
36
+ * enumerates the dispatch ledger (`listConnectorState`) and reconciles every
37
+ * outstanding instance, so completion needs no per-instance caller.
38
+ *
39
+ * The one gap that remains: **nothing calls the sweep on a timer** (#96, poll
40
+ * path). There is no cron, queue or Durable Object alarm in any deployment yet —
41
+ * the same trigger `drainDue` still lacks — so `sweepScriveReconciliations` runs
42
+ * from a test or by hand today. That trigger, not the seam or the driver, is why
43
+ * the connector stays unpublished; it is a deployment concern, not connector
44
+ * code.
45
+ */
46
+ export interface ScriveConnectorOptions {
47
+ /** `SCRIVE_TESTBED` by default; production needs a paid licence. */
48
+ baseUrl?: string;
49
+ /**
50
+ * Where Scrive should POST status changes.
51
+ *
52
+ * Scrive's callbacks are **unauthenticated** — there is no signature to
53
+ * verify — so this must be a capability URL (an unguessable secret in the
54
+ * path), and a callback must never be trusted as a fact. It is a hint to
55
+ * re-read `documents/{id}/get`. Optional because polling alone is a complete
56
+ * strategy and needs no ingress at all (#96).
57
+ */
58
+ callbackUrl?: (instanceId: string) => string;
59
+ }
60
+ /**
61
+ * What the connector remembers about a dispatch, stored per-connection in the
62
+ * directory (`ctx.admin.putConnectorState`).
63
+ *
64
+ * Two jobs. **Outbound idempotency:** a redelivery finds this row and skips
65
+ * instead of creating a second document. **The return path (#97):** the poll
66
+ * driver reads it to map a signed provider party back to the scope operation
67
+ * that records it — which needs, per party, the `requestId` it resolves and the
68
+ * `signatory` to attribute it to, plus the frozen `contentHash` `recordSignature`
69
+ * checks the provider against, and the `vertical` to reopen the connection under.
70
+ * None of that is derivable from Scrive's document, so it is captured here at
71
+ * dispatch, when the event still carries it.
72
+ */
73
+ export interface ScriveDispatchState {
74
+ documentId: string;
75
+ instanceId: string;
76
+ scopeId: string;
77
+ tenantId: string;
78
+ /** The scope's vertical — half the key that reopens the connection to poll. */
79
+ vertical: string;
80
+ /**
81
+ * The frozen content hash from `protocol.signatures-requested`. Reported back
82
+ * verbatim on record: `recordSignature` re-derives the frozen hash and refuses
83
+ * a signature whose reported hash disagrees, so the document that was signed is
84
+ * provably the document that was frozen.
85
+ */
86
+ contentHash: string;
87
+ /**
88
+ * The dispatched parties, in the order sent to Scrive — which is the order
89
+ * Scrive returns them, so the Nth provider party is this Nth entry. Carries
90
+ * what `recordSignature` cannot get from the provider: the `requestId` to
91
+ * resolve and the substrat `ref`/`kind` to attribute the signature to.
92
+ */
93
+ parties: {
94
+ requestId: string;
95
+ label: string;
96
+ kind: 'principal' | 'external';
97
+ /** The substrat signatory, when known up front; null when identity is only learned at signing. */
98
+ ref: string | null;
99
+ }[];
100
+ /** Requests already recorded by a prior poll — so a re-poll is a no-op, not a double. */
101
+ recordedRequestIds?: string[];
102
+ dispatchedAt: string;
103
+ }
104
+ /**
105
+ * Build the handler. Register it with `host.registerConnector`.
106
+ *
107
+ * Only reacts to `method: 'scrive'` — a vertical asking for BankID through
108
+ * another provider emits the same event, and this must not answer for it.
109
+ */
110
+ export declare function scriveConnector(options: ScriveConnectorOptions): ConnectorHandler;
111
+ /**
112
+ * Register the connector on a host.
113
+ *
114
+ * `maxAttempts` is deliberately higher than the executor default: a provider
115
+ * being briefly unreachable is ordinary, and giving up on a signature request
116
+ * after five tries would be giving up on a contract.
117
+ */
118
+ export declare function registerScriveConnector(host: ScopeHost, options: ScriveConnectorOptions & {
119
+ id?: string;
120
+ retry?: ConnectorOptions;
121
+ }): void;
122
+ /** The outcome of reconciling one dispatched instance against the provider. */
123
+ export interface ScriveReconcileResult {
124
+ /** The provider document reconciled. */
125
+ documentId: string;
126
+ /** Scrive's current document status (`pending`, `closed`, `rejected`, …). */
127
+ documentStatus: string;
128
+ /** Requests recorded as signed on THIS run (empty if nothing new completed). */
129
+ recorded: {
130
+ requestId: string;
131
+ signedAt: string;
132
+ }[];
133
+ /** Parties the provider reports as signed that the driver could not record, and why. */
134
+ skipped: {
135
+ requestId: string;
136
+ reason: string;
137
+ }[];
138
+ /** True once every party in the set has been recorded into the scope. */
139
+ complete: boolean;
140
+ }
141
+ /**
142
+ * The RETURN path (#97): read the provider's state for one dispatched instance
143
+ * and record any completed signatures back into the scope.
144
+ *
145
+ * This is the half the connector could not do until #97 landed. A provider's
146
+ * signature has to be written onto the protocol instance in the SCOPE, and a
147
+ * connector is not a `PrincipalId`, so `getScope` could not let it in. #97 gives
148
+ * the door a connection can walk through — `getConnectorScope(connectionId,
149
+ * scopeId)` returns a stub whose authority is the connection itself, and what it
150
+ * may do is an ordinary permission check against `connection:<id>` grants. So
151
+ * this records a signature by invoking `protocol/record-signature` on that stub;
152
+ * it works iff the connection was granted `protocol:record-signature`
153
+ * (`grantToConnection`), which appears in the permission diff like any grant.
154
+ *
155
+ * **Why a top-level function and not the dispatch handler.** A connector runs
156
+ * INSIDE the scope's dispatch, and re-entering the scope actor from there
157
+ * deadlocks (the reason dispatch idempotency lives in the directory, not the
158
+ * scope). Recording runs as its own top-level operation, outside any dispatch —
159
+ * which is exactly what a poll driver or a callback ingress is. Neither exists
160
+ * yet (nothing schedules this — issue #96); this is the reconcile step both will
161
+ * call, made correct and testable now, invoked by hand or by a test until a
162
+ * scheduler lands.
163
+ *
164
+ * Idempotent by construction: signed requests are remembered in the ledger, so a
165
+ * re-poll of a half-signed set records only what is newly done, and re-polling a
166
+ * fully-signed set records nothing.
167
+ *
168
+ * `fetch` is passed in because sanctioned egress is the host's to own and it
169
+ * exposes no top-level opener; the same `fetch` the host was built with is
170
+ * bound here to the connection (with health recorded via `recordConnectionUse`),
171
+ * mirroring what the dispatch context does internally.
172
+ */
173
+ export declare function reconcileScriveDispatch(host: ScopeHost, connectionId: ConnectionId, instanceId: string, options: {
174
+ fetch: FetchLike;
175
+ baseUrl?: string;
176
+ timeoutMs?: number;
177
+ }): Promise<ScriveReconcileResult>;
178
+ /** What one sweep of a connection's outstanding dispatches did. */
179
+ export interface ScriveSweepResult {
180
+ /** Dispatch ledger rows enumerated for the connection. */
181
+ found: number;
182
+ /** Rows the ledger already shows fully recorded — not polled against the provider. */
183
+ skipped: number;
184
+ /** Rows reconciled against the provider this sweep. */
185
+ polled: number;
186
+ /** Instances that reached "every party signed" this sweep. */
187
+ completed: string[];
188
+ /** Instances polled but still awaiting at least one signature. */
189
+ outstanding: string[];
190
+ /** Per-instance reconcile failures; the sweep continues past them. */
191
+ failed: {
192
+ instanceId: string;
193
+ error: string;
194
+ }[];
195
+ }
196
+ /**
197
+ * The SCHEDULER's unit of work (#96, poll path): reconcile every outstanding
198
+ * dispatch for one connection against the provider.
199
+ *
200
+ * `reconcileScriveDispatch` records the signatures for ONE known instance; this
201
+ * is what finds the instances. It enumerates the dispatch ledger
202
+ * (`listConnectorState(connectionId, 'scrive:dispatch:')` — the read that method
203
+ * exists for) and reconciles each row that is not already fully recorded. A
204
+ * timer calls this; it holds no timer itself. That keeps the trigger a
205
+ * deployment concern (a Cloudflare cron or Durable Object alarm, the same home
206
+ * `drainDue` still needs) and this a plain, testable function.
207
+ *
208
+ * Robust by construction, because a poller must be: a row already complete per
209
+ * the ledger is skipped without touching the provider (so a finished signature
210
+ * is not re-fetched on every tick), and a provider error on one instance is
211
+ * recorded and stepped over rather than sinking the batch. Idempotent — running
212
+ * it twice over the same state records nothing the second time.
213
+ *
214
+ * Scoped to one connection deliberately: a connection is (tenant, vertical,
215
+ * provider), so a sweep never crosses a tenant. A platform sweeper iterates the
216
+ * connections it is responsible for and calls this for each.
217
+ */
218
+ export declare function sweepScriveReconciliations(host: ScopeHost, connectionId: ConnectionId, options: {
219
+ fetch: FetchLike;
220
+ baseUrl?: string;
221
+ timeoutMs?: number;
222
+ }): Promise<ScriveSweepResult>;
223
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,353 @@
1
+ import { z } from 'zod';
2
+ import { scopeId, tenantId } from '@substrat-run/contracts';
3
+ import { ScriveApi, SCRIVE_TESTBED } from './api.js';
4
+ import { renderPdf } from './pdf.js';
5
+ export { ScriveApi, SCRIVE_TESTBED, SCRIVE_PRODUCTION } from './api.js';
6
+ export { ScriveMock } from './mock.js';
7
+ export { renderPdf } from './pdf.js';
8
+ /**
9
+ * The connector-state key prefix under which every dispatch ledger row lives —
10
+ * the handle the poll sweep enumerates by (`listConnectorState(id, prefix)`).
11
+ */
12
+ const DISPATCH_PREFIX = 'scrive:dispatch:';
13
+ /** The connector-state key for one signature request set. */
14
+ const dispatchKey = (instanceId) => `${DISPATCH_PREFIX}${instanceId}`;
15
+ /** The payload half of `protocol.signatures-requested` this connector reads. */
16
+ const signaturesRequested = z.object({
17
+ instanceId: z.string().min(1),
18
+ templateKey: z.string().min(1),
19
+ templateVersion: z.number().int(),
20
+ contentHash: z.string().min(1),
21
+ boundHash: z.string().nullable().optional(),
22
+ method: z.string().min(1),
23
+ parties: z.array(z.object({
24
+ requestId: z.string().min(1),
25
+ label: z.string().min(1),
26
+ kind: z.enum(['principal', 'external']),
27
+ ref: z.string().nullable(),
28
+ signatureKind: z.enum(['primary', 'counter']),
29
+ })),
30
+ });
31
+ /**
32
+ * Build the handler. Register it with `host.registerConnector`.
33
+ *
34
+ * Only reacts to `method: 'scrive'` — a vertical asking for BankID through
35
+ * another provider emits the same event, and this must not answer for it.
36
+ */
37
+ export function scriveConnector(options) {
38
+ const baseUrl = options.baseUrl ?? SCRIVE_TESTBED;
39
+ return async (ctx, event) => {
40
+ const payload = signaturesRequested.parse(event.payload);
41
+ if (payload.method !== 'scrive')
42
+ return; // not ours; delivered, not effected
43
+ const conn = await ctx.connection('scrive');
44
+ // Idempotency (#101 gap 3). Delivery is at-least-once, so a redelivery must
45
+ // not create a SECOND Scrive document — duplicate legal paperwork sent to
46
+ // real signatories. The connector cannot record "done" in the scope, because
47
+ // it runs INSIDE the scope's dispatch and re-entering the scope actor
48
+ // deadlocks (verified). So the dispatch ledger lives in the directory, which
49
+ // `ctx.admin` reaches without touching the scope.
50
+ const key = dispatchKey(payload.instanceId);
51
+ const prior = (await ctx.admin.getConnectorState(conn.id, key));
52
+ if (prior)
53
+ return; // already dispatched — do nothing, idempotently
54
+ const api = new ScriveApi(conn, baseUrl);
55
+ // The artifact. NOT the avtal — this connector cannot read the vertical's
56
+ // content, and should not learn its vocabulary in order to try. It renders
57
+ // an attestation sheet naming what is being signed and the hash it is
58
+ // identified by. A real contract needs the vertical's own rendering plus a
59
+ // document store, and neither exists (see README).
60
+ const pdf = renderPdf({
61
+ title: `${payload.templateKey} v${payload.templateVersion}`,
62
+ lines: [
63
+ `Instans: ${payload.instanceId}`,
64
+ `Innehållshash (SHA-256): ${payload.contentHash}`,
65
+ ...(payload.boundHash ? [`Dokumenthash: ${payload.boundHash}`] : []),
66
+ '',
67
+ 'Parter:',
68
+ ...payload.parties.map((p) => ` ${p.label} (${p.signatureKind})`),
69
+ '',
70
+ 'Signaturen avser innehållet som identifieras av hashen ovan.',
71
+ ],
72
+ });
73
+ const doc = await api.createDocument();
74
+ await api.setFile(doc.id, `${payload.templateKey}.pdf`, pdf);
75
+ await api.update(doc.id, {
76
+ title: `${payload.templateKey} v${payload.templateVersion}`,
77
+ ...(options.callbackUrl ? { callbackUrl: options.callbackUrl(payload.instanceId) } : {}),
78
+ // Tag the document with the instance id (verified settable). It is not yet
79
+ // used for dedup — the list-by-tag filter needs a query syntax not settled
80
+ // here — but it makes the eventual provider-side reconciliation that would
81
+ // close the narrow create-then-record window (below) a filter away.
82
+ tags: [{ name: 'substrat_instance', value: payload.instanceId }],
83
+ parties: payload.parties.map((p) => ({
84
+ name: p.label,
85
+ // BankID for external signatories; a principal signing through the
86
+ // provider still authenticates, but the flow does not require the
87
+ // stronger method to be meaningful.
88
+ authenticationMethodToSign: p.kind === 'external' ? 'se_bankid' : 'standard',
89
+ // Scrive auto-adds the API user as the author, and exactly one party
90
+ // must be it. The issuing (primary) party is the sender's side, so it
91
+ // is the author — and it still signs. Verified: an explicit author
92
+ // party in `update` replaces the auto one.
93
+ isAuthor: p.signatureKind === 'primary',
94
+ isSignatory: true,
95
+ })),
96
+ });
97
+ await api.start(doc.id);
98
+ // Record the dispatch so a redelivery skips it. This is the write that
99
+ // closes the duplicate hole for the common case (a retry after a fully
100
+ // successful dispatch).
101
+ //
102
+ // The residual: if this write itself fails after `start` succeeded, the
103
+ // retry finds no state and creates a second document. Closing that fully
104
+ // needs provider-side dedup — the `substrat_instance` tag set above, once a
105
+ // list-by-tag query lets the connector adopt an existing document instead of
106
+ // creating one. Left as a follow-up; a rare double is a large improvement on
107
+ // every-retry-doubles.
108
+ const state = {
109
+ documentId: doc.id,
110
+ instanceId: payload.instanceId,
111
+ scopeId: ctx.scopeId,
112
+ tenantId: ctx.tenantId,
113
+ vertical: ctx.vertical,
114
+ contentHash: payload.contentHash,
115
+ parties: payload.parties.map((p) => ({
116
+ requestId: p.requestId,
117
+ label: p.label,
118
+ kind: p.kind,
119
+ ref: p.ref,
120
+ })),
121
+ dispatchedAt: event.occurredAt,
122
+ };
123
+ await ctx.admin.putConnectorState(conn.id, key, state);
124
+ };
125
+ }
126
+ /**
127
+ * Register the connector on a host.
128
+ *
129
+ * `maxAttempts` is deliberately higher than the executor default: a provider
130
+ * being briefly unreachable is ordinary, and giving up on a signature request
131
+ * after five tries would be giving up on a contract.
132
+ */
133
+ export function registerScriveConnector(host, options) {
134
+ host.registerConnector(options.id ?? 'scrive', 'protocol.signatures-requested', scriveConnector(options), {
135
+ maxAttempts: 8,
136
+ baseDelayMs: 5_000,
137
+ maxDelayMs: 900_000,
138
+ timeoutMs: 30_000,
139
+ ...options.retry,
140
+ });
141
+ }
142
+ /**
143
+ * The RETURN path (#97): read the provider's state for one dispatched instance
144
+ * and record any completed signatures back into the scope.
145
+ *
146
+ * This is the half the connector could not do until #97 landed. A provider's
147
+ * signature has to be written onto the protocol instance in the SCOPE, and a
148
+ * connector is not a `PrincipalId`, so `getScope` could not let it in. #97 gives
149
+ * the door a connection can walk through — `getConnectorScope(connectionId,
150
+ * scopeId)` returns a stub whose authority is the connection itself, and what it
151
+ * may do is an ordinary permission check against `connection:<id>` grants. So
152
+ * this records a signature by invoking `protocol/record-signature` on that stub;
153
+ * it works iff the connection was granted `protocol:record-signature`
154
+ * (`grantToConnection`), which appears in the permission diff like any grant.
155
+ *
156
+ * **Why a top-level function and not the dispatch handler.** A connector runs
157
+ * INSIDE the scope's dispatch, and re-entering the scope actor from there
158
+ * deadlocks (the reason dispatch idempotency lives in the directory, not the
159
+ * scope). Recording runs as its own top-level operation, outside any dispatch —
160
+ * which is exactly what a poll driver or a callback ingress is. Neither exists
161
+ * yet (nothing schedules this — issue #96); this is the reconcile step both will
162
+ * call, made correct and testable now, invoked by hand or by a test until a
163
+ * scheduler lands.
164
+ *
165
+ * Idempotent by construction: signed requests are remembered in the ledger, so a
166
+ * re-poll of a half-signed set records only what is newly done, and re-polling a
167
+ * fully-signed set records nothing.
168
+ *
169
+ * `fetch` is passed in because sanctioned egress is the host's to own and it
170
+ * exposes no top-level opener; the same `fetch` the host was built with is
171
+ * bound here to the connection (with health recorded via `recordConnectionUse`),
172
+ * mirroring what the dispatch context does internally.
173
+ */
174
+ export async function reconcileScriveDispatch(host, connectionId, instanceId, options) {
175
+ const admin = host.admin;
176
+ const key = dispatchKey(instanceId);
177
+ const state = (await admin.getConnectorState(connectionId, key));
178
+ if (!state) {
179
+ throw new Error(`no scrive dispatch recorded for instance ${instanceId} on connection ${connectionId} — ` +
180
+ `nothing to reconcile`);
181
+ }
182
+ // Read the provider's truth. A callback would only be a hint to do exactly
183
+ // this; the fact is `documents/{id}/get`.
184
+ const conn = await openScriveConnection(admin, options.fetch, tenantId.parse(state.tenantId), state.vertical, options.timeoutMs ?? 30_000);
185
+ const doc = await new ScriveApi(conn, options.baseUrl ?? SCRIVE_TESTBED).get(state.documentId);
186
+ // The connection acting as itself (#97). Refuses a scope in another tenant or
187
+ // running another vertical by construction, and every write below is gated on
188
+ // the connection's own `protocol:record-signature` grant.
189
+ const scope = await host.getConnectorScope(connectionId, scopeId.parse(state.scopeId));
190
+ const recorded = [];
191
+ const skipped = [];
192
+ const done = new Set(state.recordedRequestIds ?? []);
193
+ for (const [i, party] of state.parties.entries()) {
194
+ if (done.has(party.requestId))
195
+ continue; // recorded on an earlier poll
196
+ const providerParty = doc.parties[i];
197
+ const signedAt = providerParty?.sign_time ?? null;
198
+ if (!signedAt)
199
+ continue; // not signed yet
200
+ // Fail closed on a party-order mismatch rather than attributing a signature
201
+ // to the wrong request. The connector sends exactly the party set Scrive
202
+ // keeps, in order, so index alignment holds for this model; if a provider
203
+ // ever reorders, the name disagreeing is the signal to move to name-keyed
204
+ // matching — and until then this refuses to guess.
205
+ const providerName = providerParty?.fields?.find((f) => f.type === 'name')?.value;
206
+ if (providerName !== undefined && providerName !== party.label) {
207
+ skipped.push({
208
+ requestId: party.requestId,
209
+ reason: `provider party ${i} is '${String(providerName)}', dispatch expected '${party.label}' — refusing to attribute`,
210
+ });
211
+ continue;
212
+ }
213
+ if (!party.ref) {
214
+ // The request named no signatory up front and the connector does not
215
+ // extract the signer's identity from the provider (personnummer is direct
216
+ // PII we deliberately never persist), so there is no `ref` to attribute to.
217
+ skipped.push({
218
+ requestId: party.requestId,
219
+ reason: 'provider reports a signature but the request named no signatory ref to attribute it to',
220
+ });
221
+ continue;
222
+ }
223
+ try {
224
+ await scope.invoke('protocol/record-signature', {
225
+ requestId: party.requestId,
226
+ signatory: { kind: party.kind, ref: party.ref, label: party.label },
227
+ signedAt,
228
+ // Reported verbatim; `recordSignature` checks it against the re-derived
229
+ // frozen hash and fails closed on disagreement.
230
+ contentHash: state.contentHash,
231
+ // Where the proof lives at the provider — the sealed document.
232
+ evidenceRef: `scrive:document:${state.documentId}`,
233
+ });
234
+ recorded.push({ requestId: party.requestId, signedAt });
235
+ done.add(party.requestId);
236
+ }
237
+ catch (err) {
238
+ // A request already resolved (a racing poll, or a redelivery) is not an
239
+ // error to this driver — the signature is on the instance, which is the
240
+ // goal. Anything else is real and propagates.
241
+ const msg = err instanceof Error ? err.message : String(err);
242
+ if (/already/i.test(msg)) {
243
+ done.add(party.requestId);
244
+ continue;
245
+ }
246
+ throw err;
247
+ }
248
+ }
249
+ // Remember what is recorded so a re-poll skips it without leaning on
250
+ // `recordSignature` throwing. Same row as the dispatch ledger, so the dispatch
251
+ // idempotency guard still finds it.
252
+ if (recorded.length) {
253
+ await admin.putConnectorState(connectionId, key, {
254
+ ...state,
255
+ recordedRequestIds: [...done],
256
+ });
257
+ }
258
+ return {
259
+ documentId: state.documentId,
260
+ documentStatus: doc.status,
261
+ recorded,
262
+ skipped,
263
+ complete: state.parties.every((p) => done.has(p.requestId)),
264
+ };
265
+ }
266
+ /**
267
+ * The SCHEDULER's unit of work (#96, poll path): reconcile every outstanding
268
+ * dispatch for one connection against the provider.
269
+ *
270
+ * `reconcileScriveDispatch` records the signatures for ONE known instance; this
271
+ * is what finds the instances. It enumerates the dispatch ledger
272
+ * (`listConnectorState(connectionId, 'scrive:dispatch:')` — the read that method
273
+ * exists for) and reconciles each row that is not already fully recorded. A
274
+ * timer calls this; it holds no timer itself. That keeps the trigger a
275
+ * deployment concern (a Cloudflare cron or Durable Object alarm, the same home
276
+ * `drainDue` still needs) and this a plain, testable function.
277
+ *
278
+ * Robust by construction, because a poller must be: a row already complete per
279
+ * the ledger is skipped without touching the provider (so a finished signature
280
+ * is not re-fetched on every tick), and a provider error on one instance is
281
+ * recorded and stepped over rather than sinking the batch. Idempotent — running
282
+ * it twice over the same state records nothing the second time.
283
+ *
284
+ * Scoped to one connection deliberately: a connection is (tenant, vertical,
285
+ * provider), so a sweep never crosses a tenant. A platform sweeper iterates the
286
+ * connections it is responsible for and calls this for each.
287
+ */
288
+ export async function sweepScriveReconciliations(host, connectionId, options) {
289
+ const entries = await host.admin.listConnectorState(connectionId, DISPATCH_PREFIX);
290
+ const result = {
291
+ found: entries.length,
292
+ skipped: 0,
293
+ polled: 0,
294
+ completed: [],
295
+ outstanding: [],
296
+ failed: [],
297
+ };
298
+ for (const { value } of entries) {
299
+ const state = value;
300
+ // The ledger already knows this one is done — don't poll a settled document.
301
+ const done = new Set(state.recordedRequestIds ?? []);
302
+ if (state.parties.length > 0 && state.parties.every((p) => done.has(p.requestId))) {
303
+ result.skipped += 1;
304
+ continue;
305
+ }
306
+ try {
307
+ const r = await reconcileScriveDispatch(host, connectionId, state.instanceId, options);
308
+ result.polled += 1;
309
+ (r.complete ? result.completed : result.outstanding).push(state.instanceId);
310
+ }
311
+ catch (err) {
312
+ result.failed.push({
313
+ instanceId: state.instanceId,
314
+ error: err instanceof Error ? err.message : String(err),
315
+ });
316
+ }
317
+ }
318
+ return result;
319
+ }
320
+ /**
321
+ * Open the connection with egress bound to it — the same binding the dispatch
322
+ * context makes internally, rebuilt here from public `HostAdmin` methods because
323
+ * the host exposes no top-level opener. Health lands on the right connection via
324
+ * `recordConnectionUse`, exactly as a dispatched call's does.
325
+ *
326
+ * (The cleaner home is a host method that hands a `ConnectorConnection` to any
327
+ * caller, dispatch or poll; that is a kernel addition for when the scheduler
328
+ * lands, not a precondition for the record-back path.)
329
+ */
330
+ async function openScriveConnection(admin, fetchImpl, tenant, vertical, timeoutMs) {
331
+ const open = await admin.openConnection(tenant, vertical, 'scrive');
332
+ if (!open) {
333
+ throw new Error(`no live 'scrive' connection for tenant ${tenant} / vertical '${vertical}'`);
334
+ }
335
+ return {
336
+ ...open,
337
+ fetch: async (input, init) => {
338
+ try {
339
+ const res = await fetchImpl(input, { ...init, signal: AbortSignal.timeout(timeoutMs) });
340
+ await admin.recordConnectionUse(open.id, res.ok ? { ok: true } : { ok: false, error: `HTTP ${res.status} from scrive` });
341
+ return res;
342
+ }
343
+ catch (err) {
344
+ await admin.recordConnectionUse(open.id, {
345
+ ok: false,
346
+ error: err instanceof Error ? err.message : String(err),
347
+ });
348
+ throw err;
349
+ }
350
+ },
351
+ };
352
+ }
353
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/mock.d.ts ADDED
@@ -0,0 +1,60 @@
1
+ import type { FetchLike } from '@substrat-run/kernel';
2
+ /**
3
+ * Scrive, in memory.
4
+ *
5
+ * ## What this is for
6
+ *
7
+ * A connector cannot be exercised end to end without a provider, and a provider
8
+ * account is not always available. This implements the documented endpoints so
9
+ * the seam — credential resolution, egress, health, retry, the document
10
+ * lifecycle — can be tested today.
11
+ *
12
+ * ## What it proves, and what it does not
13
+ *
14
+ * It proves OUR shape works. It cannot prove our reading of Scrive's API is
15
+ * right, because it *is* our reading of Scrive's API: same author, same
16
+ * misunderstandings, in both halves. A green suite here means "ready to check
17
+ * against a testbed account", never "verified".
18
+ *
19
+ * The specific things a mock like this will always get wrong until someone runs
20
+ * the real thing: auth handshakes, exact response shapes, error bodies, rate
21
+ * limits, and every asynchronous timing behaviour that matters.
22
+ *
23
+ * It stays useful afterwards: a real provider will not return 503 on demand, or
24
+ * let you fast-forward two days to a signature.
25
+ */
26
+ interface MockDocument {
27
+ id: string;
28
+ status: 'preparation' | 'pending' | 'closed' | 'canceled' | 'timedout' | 'rejected';
29
+ title: string;
30
+ callbackUrl: string | null;
31
+ file: {
32
+ name: string;
33
+ bytes: number;
34
+ } | null;
35
+ parties: {
36
+ id: string;
37
+ name: string;
38
+ signTime: string | null;
39
+ auth: string;
40
+ }[];
41
+ }
42
+ export interface ScriveMockOptions {
43
+ /** Reject every call with this HTTP status — the failure path on demand. */
44
+ failWith?: number;
45
+ }
46
+ export declare class ScriveMock {
47
+ readonly documents: Map<string, MockDocument>;
48
+ private seq;
49
+ failWith: number | undefined;
50
+ constructor(options?: ScriveMockOptions);
51
+ /** Simulate a party completing BankID. The provider-side event we cannot cause for real. */
52
+ sign(documentId: string, partyIndex: number, at: string): void;
53
+ decline(documentId: string): void;
54
+ private mustGet;
55
+ private wire;
56
+ /** The `fetch` to hand a host. */
57
+ get fetch(): FetchLike;
58
+ }
59
+ export {};
60
+ //# sourceMappingURL=mock.d.ts.map