@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.
- package/LICENSE +661 -0
- package/README.md +98 -0
- package/dist/api.d.ts +134 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +203 -0
- package/dist/api.js.map +1 -0
- package/dist/index.d.ts +223 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +353 -0
- package/dist/index.js.map +1 -0
- package/dist/mock.d.ts +60 -0
- package/dist/mock.d.ts.map +1 -0
- package/dist/mock.js +131 -0
- package/dist/mock.js.map +1 -0
- package/dist/pdf.d.ts +27 -0
- package/dist/pdf.d.ts.map +1 -0
- package/dist/pdf.js +87 -0
- package/dist/pdf.js.map +1 -0
- package/package.json +37 -0
package/dist/index.d.ts
ADDED
|
@@ -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
|