@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/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# @substrat-run/connector-scrive
|
|
2
|
+
|
|
3
|
+
Scrive eSign (BankID) — the **outbound half** of external signing.
|
|
4
|
+
|
|
5
|
+
`engine-protocol` emits `protocol.signatures-requested` when a vertical freezes a document and
|
|
6
|
+
sends it for signature. This turns that into a Scrive document: create → set file → set parties
|
|
7
|
+
with `se_bankid` → start.
|
|
8
|
+
|
|
9
|
+
## ⚠️ This connector cannot complete a signature
|
|
10
|
+
|
|
11
|
+
It is `private` and unpublished on purpose. Four things are missing, none of them here:
|
|
12
|
+
|
|
13
|
+
1. ~~**Dispatch is not idempotent.**~~ **Done.** A redelivery once created a *second* Scrive
|
|
14
|
+
document — duplicate legal paperwork to real signatories. The connector now records each
|
|
15
|
+
dispatch in a directory-side ledger (`ctx.admin.putConnectorState`, keyed by the connection)
|
|
16
|
+
and skips if a prior dispatch is found. Directory-side because a connector runs *inside* the
|
|
17
|
+
scope's dispatch and re-entering the scope actor deadlocks (verified). A narrow residual
|
|
18
|
+
remains — if the ledger write itself fails after the provider `start` succeeds, the retry
|
|
19
|
+
still duplicates — closable with provider-side dedup via the `substrat_instance` tag the
|
|
20
|
+
connector already sets, once a list-by-tag query lands.
|
|
21
|
+
|
|
22
|
+
2. ~~**Connector state has no home.**~~ **Done** — the ledger above is that home.
|
|
23
|
+
|
|
24
|
+
3. ~~**Recording the signature back**~~ **Done** ([#97](https://github.com/substrat-run/substrat/issues/97)).
|
|
25
|
+
When a party signs, the signature belongs on the protocol instance in the *scope*.
|
|
26
|
+
`reconcileScriveDispatch(host, connectionId, instanceId, { fetch })` reads
|
|
27
|
+
`documents/{id}/get`, maps each signed party back to its request, and records it by invoking
|
|
28
|
+
`protocol/record-signature` through `getConnectorScope` — the connection acting as itself,
|
|
29
|
+
as a top-level operation (not the dispatch handler, where re-entering the scope deadlocks).
|
|
30
|
+
The connection must hold `protocol:record-signature` (`grantToConnection`), which shows up in
|
|
31
|
+
the permission diff. Idempotent across polls; verified against `ScriveMock` advanced to
|
|
32
|
+
`closed`. The one thing a mock can't prove — Scrive's real `get` shape and party order —
|
|
33
|
+
waits on a testbed BankID round-trip (BankID-to-sign is disabled on the account).
|
|
34
|
+
|
|
35
|
+
4. **The poll driver exists; nothing calls it on a timer.**
|
|
36
|
+
`sweepScriveReconciliations(host, connectionId, { fetch })` is the scheduler's unit of work:
|
|
37
|
+
it enumerates the dispatch ledger (`HostAdmin.listConnectorState(id, 'scrive:dispatch:')`) and
|
|
38
|
+
reconciles every outstanding instance, skipping ones the ledger already shows complete and
|
|
39
|
+
stepping past a provider error on any one instance. It is idempotent and scoped to one
|
|
40
|
+
connection (a connection never crosses a tenant). What is *still* missing is the **timer** —
|
|
41
|
+
there is no cron, queue or Durable Object alarm in any wrangler config, the same trigger
|
|
42
|
+
`drainDue` still lacks. A platform sweeper would, on a schedule, iterate the connections it
|
|
43
|
+
owns and call this for each:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
// in a Cloudflare Worker's scheduled() handler, or a DO alarm:
|
|
47
|
+
for (const connectionId of scriveConnections) {
|
|
48
|
+
await sweepScriveReconciliations(host, connectionId, { fetch });
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
That trigger is a deployment concern, not connector code — which is why it, not the seam or
|
|
53
|
+
the driver, is the remaining reason the connector stays unpublished.
|
|
54
|
+
|
|
55
|
+
5. **No document store.** `attachmentTargets` is declared in the manifest contract and
|
|
56
|
+
implemented nowhere, so there is no place for rendered bytes.
|
|
57
|
+
|
|
58
|
+
## What it therefore sends
|
|
59
|
+
|
|
60
|
+
An **attestation sheet**, not the avtal: the template, the parties, and the content hash the
|
|
61
|
+
signature refers to. That is honest for a hash-attestation model and enough to exercise the
|
|
62
|
+
seam, but it is not a contract anybody should sign.
|
|
63
|
+
|
|
64
|
+
Rendering the real document belongs to the **vertical**, which owns the content — a connector
|
|
65
|
+
cannot read another module's tables and should not learn a vertical's vocabulary to try. It
|
|
66
|
+
needs (4) to hand the bytes over.
|
|
67
|
+
|
|
68
|
+
## Verified against the testbed
|
|
69
|
+
|
|
70
|
+
The API layer was **checked against `api-testbed.scrive.com`**, not just the docs — and the
|
|
71
|
+
first version, written from the docs, was wrong in three ways one live call exposed at once:
|
|
72
|
+
|
|
73
|
+
- **auth is OAuth1 PLAINTEXT**, not OAuth2 bearer (the UI's "Client" + "Token" credentials are
|
|
74
|
+
two halves of one four-part signature; the `oauth2.scrive.com` endpoint rejects them)
|
|
75
|
+
- **`documents/new` returns no `status`** — only `get` does, so mutation responses are parsed
|
|
76
|
+
for their id and status is re-read
|
|
77
|
+
- **`setfile` is `multipart/form-data`**, not a base64 body
|
|
78
|
+
|
|
79
|
+
`test/live.test.ts` runs the real lifecycle (`new → setfile → update → get`) when
|
|
80
|
+
`connectors/scrive/.dev.vars` holds a complete OAuth1 credential, and **skips** otherwise — so
|
|
81
|
+
CI without secrets stays offline and a local run with the testbed verifies the actual API. It
|
|
82
|
+
uses `standard` auth because **`se_bankid`-to-sign is disabled on the testbed account** (`start`
|
|
83
|
+
returns 409); the BankID round-trip waits on that setting.
|
|
84
|
+
|
|
85
|
+
## Testing
|
|
86
|
+
|
|
87
|
+
`ScriveMock` implements the documented endpoints in memory, so the whole lifecycle runs without
|
|
88
|
+
a provider account.
|
|
89
|
+
|
|
90
|
+
**What a mock proves:** that our shape works — credential resolution, egress, health, retry,
|
|
91
|
+
the document lifecycle.
|
|
92
|
+
|
|
93
|
+
**What it cannot prove:** that our reading of Scrive's API is correct. The mock *is* that
|
|
94
|
+
reading — same author, same misunderstandings, on both sides of the call. Green here means
|
|
95
|
+
*ready to check against `api-testbed.scrive.com`*, never *verified*.
|
|
96
|
+
|
|
97
|
+
It stays useful afterwards: a real provider will not return 503 on demand, or let you
|
|
98
|
+
fast-forward two days to a signature.
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { ConnectorConnection } from '@substrat-run/kernel';
|
|
3
|
+
/**
|
|
4
|
+
* A thin, typed client over the Scrive eSign v2 endpoints.
|
|
5
|
+
*
|
|
6
|
+
* Every call goes through the connection's `fetch`, never a global one: that is
|
|
7
|
+
* what gets it a timeout, an egress policy, and health recorded against the
|
|
8
|
+
* right connection. Module code cannot reach any of this — boundary-lint bans
|
|
9
|
+
* `fetch` outright — and a connector is host code.
|
|
10
|
+
*
|
|
11
|
+
* **The shapes here were verified against `api-testbed.scrive.com`, not just the
|
|
12
|
+
* docs.** The first version of this file was written from the documentation and
|
|
13
|
+
* was wrong in three ways a live call exposed at once (auth scheme, the upload
|
|
14
|
+
* encoding, the create-response shape). Each is called out below where it bit.
|
|
15
|
+
*/
|
|
16
|
+
export declare const SCRIVE_TESTBED = "https://api-testbed.scrive.com";
|
|
17
|
+
export declare const SCRIVE_PRODUCTION = "https://scrive.com";
|
|
18
|
+
/**
|
|
19
|
+
* A Scrive connection's credential — OAuth1 "personal access credentials".
|
|
20
|
+
*
|
|
21
|
+
* NOT OAuth2 bearer, which the first version assumed. Scrive's UI labels these
|
|
22
|
+
* "Client credentials" and "Token credentials", which reads like two schemes but
|
|
23
|
+
* is one: the four parts combine into a PLAINTEXT OAuth signature. The
|
|
24
|
+
* `oauth2.scrive.com` token endpoint rejects them with `invalid_client` — it is
|
|
25
|
+
* a different mechanism entirely.
|
|
26
|
+
*/
|
|
27
|
+
export declare const scriveSecret: z.ZodObject<{
|
|
28
|
+
clientId: z.ZodString;
|
|
29
|
+
clientSecret: z.ZodString;
|
|
30
|
+
tokenId: z.ZodString;
|
|
31
|
+
tokenSecret: z.ZodString;
|
|
32
|
+
}, z.core.$strip>;
|
|
33
|
+
export type ScriveSecret = z.infer<typeof scriveSecret>;
|
|
34
|
+
/**
|
|
35
|
+
* An id-bearing response — what `new` / `setfile` / `update` / `start` return.
|
|
36
|
+
*
|
|
37
|
+
* `POST /documents/new` returns NO top-level `status` (verified) — only
|
|
38
|
+
* `/documents/{id}/get` returns the full object. The first version parsed every
|
|
39
|
+
* response as a full document and would have thrown on call one. So mutation
|
|
40
|
+
* responses are parsed for their id only, and status is read from `get` — which
|
|
41
|
+
* is the right design anyway: don't trust a mutation's echo, re-read the truth.
|
|
42
|
+
*/
|
|
43
|
+
export declare const scriveDocRef: z.ZodObject<{
|
|
44
|
+
id: z.ZodString;
|
|
45
|
+
}, z.core.$strip>;
|
|
46
|
+
export type ScriveDocRef = z.infer<typeof scriveDocRef>;
|
|
47
|
+
/** The full document, as `get` returns it — extra fields ignored. */
|
|
48
|
+
export declare const scriveDocument: z.ZodObject<{
|
|
49
|
+
id: z.ZodString;
|
|
50
|
+
status: z.ZodEnum<{
|
|
51
|
+
preparation: "preparation";
|
|
52
|
+
pending: "pending";
|
|
53
|
+
closed: "closed";
|
|
54
|
+
canceled: "canceled";
|
|
55
|
+
timedout: "timedout";
|
|
56
|
+
rejected: "rejected";
|
|
57
|
+
}>;
|
|
58
|
+
parties: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
59
|
+
id: z.ZodString;
|
|
60
|
+
is_author: z.ZodOptional<z.ZodBoolean>;
|
|
61
|
+
is_signatory: z.ZodOptional<z.ZodBoolean>;
|
|
62
|
+
signatory_role: z.ZodOptional<z.ZodString>;
|
|
63
|
+
sign_time: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
64
|
+
authentication_method_to_sign: z.ZodOptional<z.ZodString>;
|
|
65
|
+
fields: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
66
|
+
type: z.ZodString;
|
|
67
|
+
value: z.ZodUnknown;
|
|
68
|
+
}, z.core.$strip>>>;
|
|
69
|
+
}, z.core.$strip>>>;
|
|
70
|
+
}, z.core.$strip>;
|
|
71
|
+
export type ScriveDocument = z.infer<typeof scriveDocument>;
|
|
72
|
+
export interface ScriveParty {
|
|
73
|
+
/** Display name for the signing page. */
|
|
74
|
+
name: string;
|
|
75
|
+
email?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Swedish personnummer, when the flow authenticates to sign with BankID.
|
|
78
|
+
*
|
|
79
|
+
* Passed THROUGH to the provider and never persisted by us: it is `direct`
|
|
80
|
+
* PII, and `engine-protocol` stores an opaque `DataSubjectId` as the signatory
|
|
81
|
+
* instead. The provider needs it; our tables must not have it.
|
|
82
|
+
*/
|
|
83
|
+
personalNumber?: string;
|
|
84
|
+
/** `se_bankid` for Swedish BankID; `standard` otherwise. */
|
|
85
|
+
authenticationMethodToSign: 'standard' | 'se_bankid';
|
|
86
|
+
/**
|
|
87
|
+
* The sender/author. Scrive auto-adds the API user as an author party on
|
|
88
|
+
* `new`; exactly one party across the set must be the author, so the connector
|
|
89
|
+
* marks the issuing (primary) party as it. Verified: sending an explicit
|
|
90
|
+
* author party in `update` replaces the auto one.
|
|
91
|
+
*/
|
|
92
|
+
isAuthor?: boolean;
|
|
93
|
+
/** A viewer rather than a signer — an author who does not sign. */
|
|
94
|
+
isSignatory?: boolean;
|
|
95
|
+
}
|
|
96
|
+
export declare class ScriveApi {
|
|
97
|
+
private readonly conn;
|
|
98
|
+
private readonly baseUrl;
|
|
99
|
+
private readonly secret;
|
|
100
|
+
constructor(conn: ConnectorConnection, baseUrl?: string);
|
|
101
|
+
/**
|
|
102
|
+
* The OAuth1 PLAINTEXT authorization header. The signature is
|
|
103
|
+
* `<clientSecret>&<tokenSecret>` — literally the two secrets joined by `&`,
|
|
104
|
+
* which is what "PLAINTEXT" means: no HMAC, TLS is the confidentiality.
|
|
105
|
+
*/
|
|
106
|
+
private headers;
|
|
107
|
+
createDocument(): Promise<ScriveDocRef>;
|
|
108
|
+
/**
|
|
109
|
+
* Attach the PDF. **`multipart/form-data`**, verified — not the raw base64 body
|
|
110
|
+
* the first version sent. The multipart envelope is built as bytes because the
|
|
111
|
+
* file is binary and a string body would corrupt it (which is why
|
|
112
|
+
* `ConnectorRequestInit.body` accepts `Uint8Array`).
|
|
113
|
+
*/
|
|
114
|
+
setFile(documentId: string, filename: string, pdf: Uint8Array): Promise<void>;
|
|
115
|
+
/** Parties, callback URL and title, in one `document=` form field. */
|
|
116
|
+
update(documentId: string, patch: {
|
|
117
|
+
title?: string;
|
|
118
|
+
parties?: ScriveParty[];
|
|
119
|
+
callbackUrl?: string;
|
|
120
|
+
tags?: {
|
|
121
|
+
name: string;
|
|
122
|
+
value: string;
|
|
123
|
+
}[];
|
|
124
|
+
}): Promise<ScriveDocRef>;
|
|
125
|
+
/** Send it. After this the document is `pending` and the parties are invited. */
|
|
126
|
+
start(documentId: string): Promise<ScriveDocRef>;
|
|
127
|
+
/**
|
|
128
|
+
* Current state — the polling path, and the only call that returns `status`.
|
|
129
|
+
* Webhook ingress (#96) is not on the critical path precisely because this
|
|
130
|
+
* exists and Scrive's callbacks are unauthenticated anyway.
|
|
131
|
+
*/
|
|
132
|
+
get(documentId: string): Promise<ScriveDocument>;
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAMhE;;;;;;;;;;;;GAYG;AAEH,eAAO,MAAM,cAAc,mCAAmC,CAAC;AAC/D,eAAO,MAAM,iBAAiB,uBAAuB,CAAC;AAEtD;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY;;;;;iBAKvB,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAExD;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY;;iBAAsC,CAAC;AAChE,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAExD,qEAAqE;AACrE,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;iBAyBzB,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAE5D,MAAM,WAAW,WAAW;IAC1B,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,4DAA4D;IAC5D,0BAA0B,EAAE,UAAU,GAAG,WAAW,CAAC;IACrD;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,mEAAmE;IACnE,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AA2BD,qBAAa,SAAS;IAIlB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAJ1B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;gBAGnB,IAAI,EAAE,mBAAmB,EACzB,OAAO,GAAE,MAAuB;IAKnD;;;;OAIG;IACH,OAAO,CAAC,OAAO;IAUT,cAAc,IAAI,OAAO,CAAC,YAAY,CAAC;IAQ7C;;;;;OAKG;IACG,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAWnF,sEAAsE;IAChE,MAAM,CACV,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE;QACL,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;QACxB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,IAAI,CAAC,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAA;SAAE,EAAE,CAAC;KAC1C,GACA,OAAO,CAAC,YAAY,CAAC;IAiCxB,iFAAiF;IAC3E,KAAK,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAQtD;;;;OAIG;IACG,GAAG,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;CAOvD"}
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* A thin, typed client over the Scrive eSign v2 endpoints.
|
|
4
|
+
*
|
|
5
|
+
* Every call goes through the connection's `fetch`, never a global one: that is
|
|
6
|
+
* what gets it a timeout, an egress policy, and health recorded against the
|
|
7
|
+
* right connection. Module code cannot reach any of this — boundary-lint bans
|
|
8
|
+
* `fetch` outright — and a connector is host code.
|
|
9
|
+
*
|
|
10
|
+
* **The shapes here were verified against `api-testbed.scrive.com`, not just the
|
|
11
|
+
* docs.** The first version of this file was written from the documentation and
|
|
12
|
+
* was wrong in three ways a live call exposed at once (auth scheme, the upload
|
|
13
|
+
* encoding, the create-response shape). Each is called out below where it bit.
|
|
14
|
+
*/
|
|
15
|
+
export const SCRIVE_TESTBED = 'https://api-testbed.scrive.com';
|
|
16
|
+
export const SCRIVE_PRODUCTION = 'https://scrive.com';
|
|
17
|
+
/**
|
|
18
|
+
* A Scrive connection's credential — OAuth1 "personal access credentials".
|
|
19
|
+
*
|
|
20
|
+
* NOT OAuth2 bearer, which the first version assumed. Scrive's UI labels these
|
|
21
|
+
* "Client credentials" and "Token credentials", which reads like two schemes but
|
|
22
|
+
* is one: the four parts combine into a PLAINTEXT OAuth signature. The
|
|
23
|
+
* `oauth2.scrive.com` token endpoint rejects them with `invalid_client` — it is
|
|
24
|
+
* a different mechanism entirely.
|
|
25
|
+
*/
|
|
26
|
+
export const scriveSecret = z.object({
|
|
27
|
+
clientId: z.string().min(1),
|
|
28
|
+
clientSecret: z.string().min(1),
|
|
29
|
+
tokenId: z.string().min(1),
|
|
30
|
+
tokenSecret: z.string().min(1),
|
|
31
|
+
});
|
|
32
|
+
/**
|
|
33
|
+
* An id-bearing response — what `new` / `setfile` / `update` / `start` return.
|
|
34
|
+
*
|
|
35
|
+
* `POST /documents/new` returns NO top-level `status` (verified) — only
|
|
36
|
+
* `/documents/{id}/get` returns the full object. The first version parsed every
|
|
37
|
+
* response as a full document and would have thrown on call one. So mutation
|
|
38
|
+
* responses are parsed for their id only, and status is read from `get` — which
|
|
39
|
+
* is the right design anyway: don't trust a mutation's echo, re-read the truth.
|
|
40
|
+
*/
|
|
41
|
+
export const scriveDocRef = z.object({ id: z.string().min(1) });
|
|
42
|
+
/** The full document, as `get` returns it — extra fields ignored. */
|
|
43
|
+
export const scriveDocument = z.object({
|
|
44
|
+
id: z.string().min(1),
|
|
45
|
+
status: z.enum(['preparation', 'pending', 'closed', 'canceled', 'timedout', 'rejected']),
|
|
46
|
+
parties: z
|
|
47
|
+
.array(z.object({
|
|
48
|
+
id: z.string().min(1),
|
|
49
|
+
is_author: z.boolean().optional(),
|
|
50
|
+
is_signatory: z.boolean().optional(),
|
|
51
|
+
signatory_role: z.string().optional(),
|
|
52
|
+
/** Set once that party has signed. */
|
|
53
|
+
sign_time: z.string().nullable().optional(),
|
|
54
|
+
authentication_method_to_sign: z.string().optional(),
|
|
55
|
+
/**
|
|
56
|
+
* The party's fields — read by the poll driver to cross-check that the
|
|
57
|
+
* provider's Nth party is still the dispatch's Nth party (name), before
|
|
58
|
+
* attributing a signature to a request. Kept so the reconcile can fail
|
|
59
|
+
* closed on a reorder rather than mis-record.
|
|
60
|
+
*/
|
|
61
|
+
fields: z
|
|
62
|
+
.array(z.object({ type: z.string(), value: z.unknown() }))
|
|
63
|
+
.optional(),
|
|
64
|
+
}))
|
|
65
|
+
.default([]),
|
|
66
|
+
});
|
|
67
|
+
const asJson = async (res, what) => {
|
|
68
|
+
const body = await res.text();
|
|
69
|
+
if (!res.ok) {
|
|
70
|
+
// Scrive's error body is JSON with `error_message`; surface it rather than a
|
|
71
|
+
// bare status, because "This feature is disabled" is the difference between a
|
|
72
|
+
// bug and an account setting.
|
|
73
|
+
let detail = body.slice(0, 400);
|
|
74
|
+
try {
|
|
75
|
+
const parsed = JSON.parse(body);
|
|
76
|
+
if (parsed.error_message)
|
|
77
|
+
detail = parsed.error_message;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
/* not JSON; keep the raw slice */
|
|
81
|
+
}
|
|
82
|
+
throw new Error(`scrive ${what} failed: HTTP ${res.status} ${detail}`);
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
return JSON.parse(body);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
throw new Error(`scrive ${what}: response was not JSON (${body.slice(0, 200)})`);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
export class ScriveApi {
|
|
92
|
+
conn;
|
|
93
|
+
baseUrl;
|
|
94
|
+
secret;
|
|
95
|
+
constructor(conn, baseUrl = SCRIVE_TESTBED) {
|
|
96
|
+
this.conn = conn;
|
|
97
|
+
this.baseUrl = baseUrl;
|
|
98
|
+
this.secret = scriveSecret.parse(conn.secret);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The OAuth1 PLAINTEXT authorization header. The signature is
|
|
102
|
+
* `<clientSecret>&<tokenSecret>` — literally the two secrets joined by `&`,
|
|
103
|
+
* which is what "PLAINTEXT" means: no HMAC, TLS is the confidentiality.
|
|
104
|
+
*/
|
|
105
|
+
headers(extra = {}) {
|
|
106
|
+
const s = this.secret;
|
|
107
|
+
const auth = `oauth_signature_method="PLAINTEXT", ` +
|
|
108
|
+
`oauth_consumer_key="${s.clientId}", ` +
|
|
109
|
+
`oauth_token="${s.tokenId}", ` +
|
|
110
|
+
`oauth_signature="${s.clientSecret}&${s.tokenSecret}"`;
|
|
111
|
+
return { authorization: auth, ...extra };
|
|
112
|
+
}
|
|
113
|
+
async createDocument() {
|
|
114
|
+
const res = await this.conn.fetch(`${this.baseUrl}/api/v2/documents/new`, {
|
|
115
|
+
method: 'POST',
|
|
116
|
+
headers: this.headers(),
|
|
117
|
+
});
|
|
118
|
+
return scriveDocRef.parse(await asJson(res, 'documents/new'));
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Attach the PDF. **`multipart/form-data`**, verified — not the raw base64 body
|
|
122
|
+
* the first version sent. The multipart envelope is built as bytes because the
|
|
123
|
+
* file is binary and a string body would corrupt it (which is why
|
|
124
|
+
* `ConnectorRequestInit.body` accepts `Uint8Array`).
|
|
125
|
+
*/
|
|
126
|
+
async setFile(documentId, filename, pdf) {
|
|
127
|
+
const boundary = `----substrat${filename.length}${pdf.length}`;
|
|
128
|
+
const body = multipartFile(boundary, 'file', filename, pdf);
|
|
129
|
+
const res = await this.conn.fetch(`${this.baseUrl}/api/v2/documents/${documentId}/setfile`, {
|
|
130
|
+
method: 'POST',
|
|
131
|
+
headers: this.headers({ 'content-type': `multipart/form-data; boundary=${boundary}` }),
|
|
132
|
+
body,
|
|
133
|
+
});
|
|
134
|
+
await asJson(res, 'setfile');
|
|
135
|
+
}
|
|
136
|
+
/** Parties, callback URL and title, in one `document=` form field. */
|
|
137
|
+
async update(documentId, patch) {
|
|
138
|
+
const document = {
|
|
139
|
+
...(patch.title ? { title: patch.title } : {}),
|
|
140
|
+
...(patch.callbackUrl ? { api_callback_url: patch.callbackUrl } : {}),
|
|
141
|
+
...(patch.tags ? { tags: patch.tags } : {}),
|
|
142
|
+
...(patch.parties
|
|
143
|
+
? {
|
|
144
|
+
parties: patch.parties.map((p) => ({
|
|
145
|
+
is_author: p.isAuthor ?? false,
|
|
146
|
+
is_signatory: p.isSignatory ?? true,
|
|
147
|
+
authentication_method_to_sign: p.authenticationMethodToSign,
|
|
148
|
+
fields: [
|
|
149
|
+
{ type: 'name', order: 1, value: p.name },
|
|
150
|
+
...(p.email ? [{ type: 'email', value: p.email }] : []),
|
|
151
|
+
...(p.personalNumber
|
|
152
|
+
? [{ type: 'personal_number', value: p.personalNumber }]
|
|
153
|
+
: []),
|
|
154
|
+
],
|
|
155
|
+
})),
|
|
156
|
+
}
|
|
157
|
+
: {}),
|
|
158
|
+
};
|
|
159
|
+
// Scrive takes the document JSON as a url-encoded `document=` form field, not
|
|
160
|
+
// a JSON request body — another shape the docs left ambiguous and the testbed
|
|
161
|
+
// settled.
|
|
162
|
+
const res = await this.conn.fetch(`${this.baseUrl}/api/v2/documents/${documentId}/update`, {
|
|
163
|
+
method: 'POST',
|
|
164
|
+
headers: this.headers({ 'content-type': 'application/x-www-form-urlencoded' }),
|
|
165
|
+
body: `document=${encodeURIComponent(JSON.stringify(document))}`,
|
|
166
|
+
});
|
|
167
|
+
return scriveDocRef.parse(await asJson(res, 'update'));
|
|
168
|
+
}
|
|
169
|
+
/** Send it. After this the document is `pending` and the parties are invited. */
|
|
170
|
+
async start(documentId) {
|
|
171
|
+
const res = await this.conn.fetch(`${this.baseUrl}/api/v2/documents/${documentId}/start`, {
|
|
172
|
+
method: 'POST',
|
|
173
|
+
headers: this.headers(),
|
|
174
|
+
});
|
|
175
|
+
return scriveDocRef.parse(await asJson(res, 'start'));
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Current state — the polling path, and the only call that returns `status`.
|
|
179
|
+
* Webhook ingress (#96) is not on the critical path precisely because this
|
|
180
|
+
* exists and Scrive's callbacks are unauthenticated anyway.
|
|
181
|
+
*/
|
|
182
|
+
async get(documentId) {
|
|
183
|
+
const res = await this.conn.fetch(`${this.baseUrl}/api/v2/documents/${documentId}/get`, {
|
|
184
|
+
method: 'GET',
|
|
185
|
+
headers: this.headers(),
|
|
186
|
+
});
|
|
187
|
+
return scriveDocument.parse(await asJson(res, 'get'));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/** A one-file `multipart/form-data` body, as bytes. Web-standard, no node:buffer. */
|
|
191
|
+
function multipartFile(boundary, field, filename, file) {
|
|
192
|
+
const enc = new TextEncoder();
|
|
193
|
+
const head = enc.encode(`--${boundary}\r\n` +
|
|
194
|
+
`content-disposition: form-data; name="${field}"; filename="${filename}"\r\n` +
|
|
195
|
+
`content-type: application/pdf\r\n\r\n`);
|
|
196
|
+
const tail = enc.encode(`\r\n--${boundary}--\r\n`);
|
|
197
|
+
const out = new Uint8Array(head.length + file.length + tail.length);
|
|
198
|
+
out.set(head, 0);
|
|
199
|
+
out.set(file, head.length);
|
|
200
|
+
out.set(tail, head.length + file.length);
|
|
201
|
+
return out;
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=api.js.map
|
package/dist/api.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
|