@paper19/gcforms-client 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 gcforms-client contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,155 @@
1
+ # gcforms-client
2
+
3
+ > [!IMPORTANT]
4
+ > This is an unofficial, community-maintained client. It is not a Government of Canada project and is not affiliated with, maintained, or endorsed by the GC Forms team or the Canadian Digital Service.
5
+
6
+ TypeScript client for the [GC Forms](https://articles.alpha.canada.ca/forms-formulaires/) (Government of Canada Forms) API. Handles OAuth2 JWT-bearer authentication against the GC Forms identity provider, retrieval and decryption of form submissions (RSA-OAEP + AES-256-GCM), integrity verification, attachment downloads, and confirmation — with built-in retries and typed errors.
7
+
8
+ Requires Node.js 20 or later. ESM-only.
9
+
10
+ ## Install
11
+
12
+ ```sh
13
+ npm install @paper19/gcforms-client
14
+ ```
15
+
16
+ ## Quick start
17
+
18
+ Generate an API key for your form in GC Forms under **Settings → API integration**. This downloads a `<formId>_private_api_key.json` file — it is the only configuration the client needs.
19
+
20
+ ```ts
21
+ import { readFile } from "node:fs/promises";
22
+ import { GcFormsClient, gcFormsCredentialsSchema } from "@paper19/gcforms-client";
23
+
24
+ const credentials = gcFormsCredentialsSchema.parse(
25
+ JSON.parse(await readFile("./<formId>_private_api_key.json", "utf8")),
26
+ );
27
+
28
+ const client = new GcFormsClient({ credentials });
29
+
30
+ // Up to the 100 oldest submissions still in "New" status.
31
+ const newSubmissions = await client.getNewSubmissions();
32
+
33
+ for (const { name } of newSubmissions) {
34
+ // Fetches, decrypts and integrity-checks the submission.
35
+ const submission = await client.getSubmission(name);
36
+ const answers = JSON.parse(submission.answers);
37
+
38
+ // Attachment download links are only valid for ~10 seconds after
39
+ // retrieval — download immediately, before doing anything else.
40
+ for (const attachment of submission.attachments ?? []) {
41
+ if (attachment.isPotentiallyMalicious) continue; // flagged by GC Forms scanning
42
+ const bytes = await client.downloadAttachment(attachment);
43
+ // ...persist bytes...
44
+ }
45
+
46
+ // ...persist answers...
47
+
48
+ // Only confirm once everything is safely stored: confirmed submissions
49
+ // are deleted from GC Forms after 30 days.
50
+ await client.confirmSubmission(name, submission.confirmationCode);
51
+ }
52
+ ```
53
+
54
+ ### Loading the key from Azure Key Vault
55
+
56
+ ```sh
57
+ az keyvault secret set \
58
+ --vault-name <vault-name> \
59
+ --name gcforms-private-api-key \
60
+ --file <formId>_private_api_key.json
61
+ ```
62
+
63
+ ```ts
64
+ import { DefaultAzureCredential } from "@azure/identity";
65
+ import { SecretClient } from "@azure/keyvault-secrets";
66
+ import { GcFormsClient, gcFormsCredentialsSchema } from "@paper19/gcforms-client";
67
+
68
+ const vault = new SecretClient(
69
+ "https://<vault-name>.vault.azure.net",
70
+ new DefaultAzureCredential(),
71
+ );
72
+
73
+ const secret = await vault.getSecret("gcforms-private-api-key");
74
+ if (!secret.value) {
75
+ throw new Error("Secret gcforms-private-api-key has no value");
76
+ }
77
+
78
+ const credentials = gcFormsCredentialsSchema.parse(JSON.parse(secret.value));
79
+ const client = new GcFormsClient({ credentials });
80
+ ```
81
+
82
+ ## API
83
+
84
+ | Method | Description |
85
+ | --- | --- |
86
+ | `getFormTemplate()` | Form structure and question definitions (shape depends on the form). |
87
+ | `getNewSubmissions()` | Up to the 100 oldest submissions in `New` status. |
88
+ | `getSubmission(name)` | Fetches, decrypts and integrity-checks one submission. |
89
+ | `getEncryptedSubmission(name)` | The raw encrypted envelope, if you want to decrypt later/elsewhere. |
90
+ | `downloadAttachment(attachment)` | Downloads via the pre-signed link (valid ~10 s) → `ArrayBuffer`. |
91
+ | `confirmSubmission(name, confirmationCode)` | Marks a submission as received. |
92
+ | `reportProblem(name, problem)` | Flags a submission for GC Forms support review. |
93
+
94
+ Standalone helpers `decryptFormSubmission(encrypted, credentials)` and `verifySubmissionIntegrity(answers, checksum)` are also exported, along with all zod schemas and inferred types (`formSubmissionSchema`, `FormSubmission`, …).
95
+
96
+ ## Configuration
97
+
98
+ All fields except `credentials` are optional; the defaults point at the production GC Forms service.
99
+
100
+ ```ts
101
+ new GcFormsClient({
102
+ credentials, // required — parsed <formId>_private_api_key.json
103
+ apiUrl, // default "https://api.forms-formulaires.alpha.canada.ca"
104
+ identityProviderUrl, // default "https://auth.forms-formulaires.alpha.canada.ca"
105
+ projectIdentifier, // default "284778202772022819" (GC Forms Zitadel project)
106
+ apiVersion, // default "v1"
107
+ timeoutMs, // default 10_000 — per request attempt
108
+ retry: {
109
+ maxAttempts, // default 3 (total attempts; 1 disables retries)
110
+ baseDelayMs, // default 250
111
+ maxDelayMs, // default 8_000
112
+ retryStatuses, // default [429, 500, 502, 503, 504]
113
+ },
114
+ });
115
+ ```
116
+
117
+ Retries use exponential backoff with full jitter and honour `Retry-After` (capped at 30 s). Network errors and timeouts are retried; a `401` triggers a single re-authentication with a fresh token. Access tokens are cached per client instance and refreshed shortly before expiry.
118
+
119
+ ## Error handling
120
+
121
+ Everything the package throws (other than zod validation errors on your own inputs) extends `GcFormsApiError`:
122
+
123
+ ```ts
124
+ import { GcFormsApiError, GcFormsAuthError, GcFormsDecryptionError } from "@paper19/gcforms-client";
125
+
126
+ try {
127
+ await client.getSubmission(name);
128
+ } catch (error) {
129
+ if (error instanceof GcFormsAuthError) {
130
+ // token could not be obtained from the identity provider
131
+ } else if (error instanceof GcFormsDecryptionError) {
132
+ // decryption failed or the checksum did not match
133
+ } else if (error instanceof GcFormsApiError) {
134
+ console.error(error.status, error.responseBody, error.cause);
135
+ }
136
+ }
137
+ ```
138
+
139
+ ## Security notes
140
+
141
+ - The `key` in the private API key file is both the OAuth signing key and the submission decryption key — treat the whole file as a secret (do not commit it; load it from your secret store).
142
+ - Check `attachment.isPotentiallyMalicious` before using attachments; it is set by GC Forms' malware scanning.
143
+ - The MD5 `checksum` is the service's integrity check, not a security boundary — authenticity is provided by the AES-GCM auth tag.
144
+ - Confirm a submission only after its answers and attachments are durably persisted; confirmed submissions are deleted from GC Forms after 30 days.
145
+
146
+ ## Development
147
+
148
+ ```sh
149
+ npm install
150
+ npm test # vitest
151
+ npm run typecheck # tsc over src + tests
152
+ npm run build # emits dist/ (ESM + .d.ts)
153
+ ```
154
+
155
+ Publishing: `npm version <patch|minor|major> && npm publish` (the `prepack` script builds `dist/` automatically).
package/dist/auth.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ import { type RetryOptions } from "./retry.js";
2
+ import type { GcFormsCredentials } from "./types.js";
3
+ export type GcFormsAuthConfig = {
4
+ identityProviderUrl: string;
5
+ projectIdentifier: string;
6
+ credentials: GcFormsCredentials;
7
+ timeoutMs: number;
8
+ retry: RetryOptions;
9
+ };
10
+ /** RS256-signed OAuth JWT-bearer assertion, as required by the GC Forms IdP (Zitadel). */
11
+ export declare function signJwtAssertion({ identityProviderUrl, credentials, }: Pick<GcFormsAuthConfig, "identityProviderUrl" | "credentials">): string;
12
+ /**
13
+ * Fetches and caches bearer tokens for the GC Forms API. Tokens are reused
14
+ * until shortly before expiry (they are valid for 30 minutes), and
15
+ * concurrent refreshes are coalesced into a single token request.
16
+ */
17
+ export declare class TokenProvider {
18
+ private readonly config;
19
+ private cached;
20
+ private inflight;
21
+ constructor(config: GcFormsAuthConfig);
22
+ getAccessToken(): Promise<string>;
23
+ /** Drops the cached token so the next call fetches a fresh one. */
24
+ invalidate(): void;
25
+ private fetchToken;
26
+ }
27
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAGA,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAWrD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,WAAW,EAAE,kBAAkB,CAAC;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,YAAY,CAAC;CACrB,CAAC;AAEF,0FAA0F;AAC1F,wBAAgB,gBAAgB,CAAC,EAC/B,mBAAmB,EACnB,WAAW,GACZ,EAAE,IAAI,CAAC,iBAAiB,EAAE,qBAAqB,GAAG,aAAa,CAAC,GAAG,MAAM,CAwBzE;AAQD;;;;GAIG;AACH,qBAAa,aAAa;IAIL,OAAO,CAAC,QAAQ,CAAC,MAAM;IAH1C,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,QAAQ,CAA8B;gBAEV,MAAM,EAAE,iBAAiB;IAEhD,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC;IAc9C,mEAAmE;IAC5D,UAAU,IAAI,IAAI;YAIX,UAAU;CAwDzB"}
package/dist/auth.js ADDED
@@ -0,0 +1,94 @@
1
+ import { createPrivateKey, sign } from "node:crypto";
2
+ import { z } from "zod";
3
+ import { GcFormsAuthError } from "./errors.js";
4
+ import { fetchWithRetry } from "./retry.js";
5
+ const ASSERTION_LIFETIME_SECONDS = 60;
6
+ const TOKEN_REFRESH_MARGIN_MS = 60_000;
7
+ const DEFAULT_TOKEN_LIFETIME_SECONDS = 30 * 60;
8
+ const tokenResponseSchema = z.object({
9
+ access_token: z.string().min(1),
10
+ expires_in: z.number().default(DEFAULT_TOKEN_LIFETIME_SECONDS),
11
+ });
12
+ /** RS256-signed OAuth JWT-bearer assertion, as required by the GC Forms IdP (Zitadel). */
13
+ export function signJwtAssertion({ identityProviderUrl, credentials, }) {
14
+ const issuedAt = Math.floor(Date.now() / 1000);
15
+ const header = base64UrlJson({
16
+ alg: "RS256",
17
+ typ: "JWT",
18
+ kid: credentials.keyId,
19
+ });
20
+ const payload = base64UrlJson({
21
+ iss: credentials.userId,
22
+ sub: credentials.userId,
23
+ aud: identityProviderUrl,
24
+ iat: issuedAt,
25
+ exp: issuedAt + ASSERTION_LIFETIME_SECONDS,
26
+ });
27
+ const signingInput = `${header}.${payload}`;
28
+ const signature = sign("sha256", Buffer.from(signingInput), createPrivateKey(credentials.key)).toString("base64url");
29
+ return `${signingInput}.${signature}`;
30
+ }
31
+ function base64UrlJson(value) {
32
+ return Buffer.from(JSON.stringify(value)).toString("base64url");
33
+ }
34
+ /**
35
+ * Fetches and caches bearer tokens for the GC Forms API. Tokens are reused
36
+ * until shortly before expiry (they are valid for 30 minutes), and
37
+ * concurrent refreshes are coalesced into a single token request.
38
+ */
39
+ export class TokenProvider {
40
+ config;
41
+ cached;
42
+ inflight;
43
+ constructor(config) {
44
+ this.config = config;
45
+ }
46
+ async getAccessToken() {
47
+ if (this.cached &&
48
+ this.cached.expiresAt - TOKEN_REFRESH_MARGIN_MS > Date.now()) {
49
+ return this.cached.value;
50
+ }
51
+ this.inflight ??= this.fetchToken().finally(() => {
52
+ this.inflight = undefined;
53
+ });
54
+ return this.inflight;
55
+ }
56
+ /** Drops the cached token so the next call fetches a fresh one. */
57
+ invalidate() {
58
+ this.cached = undefined;
59
+ }
60
+ async fetchToken() {
61
+ let response;
62
+ try {
63
+ response = await fetchWithRetry(`${this.config.identityProviderUrl}/oauth/v2/token`, () => ({
64
+ method: "POST",
65
+ body: new URLSearchParams({
66
+ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
67
+ assertion: signJwtAssertion(this.config),
68
+ scope: `openid profile urn:zitadel:iam:org:project:id:${this.config.projectIdentifier}:aud`,
69
+ }),
70
+ }), this.config.timeoutMs, this.config.retry);
71
+ }
72
+ catch (error) {
73
+ throw new GcFormsAuthError("Failed to obtain GC Forms access token", undefined, undefined, { cause: error });
74
+ }
75
+ if (!response.ok) {
76
+ const responseBody = await response.text().catch(() => undefined);
77
+ throw new GcFormsAuthError(`Failed to obtain GC Forms access token (HTTP ${response.status})`, response.status, responseBody);
78
+ }
79
+ let access_token;
80
+ let expires_in;
81
+ try {
82
+ ({ access_token, expires_in } = tokenResponseSchema.parse(await response.json()));
83
+ }
84
+ catch (error) {
85
+ throw new GcFormsAuthError("GC Forms identity provider returned a malformed token response", undefined, undefined, { cause: error });
86
+ }
87
+ this.cached = {
88
+ value: access_token,
89
+ expiresAt: Date.now() + expires_in * 1000,
90
+ };
91
+ return access_token;
92
+ }
93
+ }
94
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,57 @@
1
+ import { type RetryOptions } from "./retry.js";
2
+ import { type Attachment, type EncryptedFormSubmission, type FormSubmission, type FormSubmissionProblem, type NewFormSubmission, type GcFormsCredentials } from "./types.js";
3
+ export declare const DEFAULT_API_URL = "https://api.forms-formulaires.alpha.canada.ca";
4
+ export declare const DEFAULT_IDENTITY_PROVIDER_URL = "https://auth.forms-formulaires.alpha.canada.ca";
5
+ export declare const DEFAULT_PROJECT_IDENTIFIER = "284778202772022819";
6
+ export type GcFormsClientConfig = {
7
+ /** Contents of the `<formId>_private_api_key.json` file from GC Forms. */
8
+ credentials: GcFormsCredentials;
9
+ /** @default DEFAULT_API_URL (the production GC Forms API) */
10
+ apiUrl?: string;
11
+ /** @default DEFAULT_IDENTITY_PROVIDER_URL (the production GC Forms IdP) */
12
+ identityProviderUrl?: string;
13
+ /** @default DEFAULT_PROJECT_IDENTIFIER (the production GC Forms Zitadel project) */
14
+ projectIdentifier?: string;
15
+ /** API version path segment. @default "v1" */
16
+ apiVersion?: string;
17
+ /** Per-attempt request timeout. @default 10_000 */
18
+ timeoutMs?: number;
19
+ retry?: RetryOptions;
20
+ };
21
+ export declare class GcFormsClient {
22
+ /** The form this client is bound to, taken from the private API key. */
23
+ readonly formId: string;
24
+ private readonly baseUrl;
25
+ private readonly credentials;
26
+ private readonly timeoutMs;
27
+ private readonly retry;
28
+ private readonly tokenProvider;
29
+ constructor(config: GcFormsClientConfig);
30
+ /** Form structure and question definitions; shape depends on the form. */
31
+ getFormTemplate(): Promise<Record<string, unknown>>;
32
+ /** Up to the 100 oldest submissions still in "New" status. */
33
+ getNewSubmissions(): Promise<NewFormSubmission[]>;
34
+ getEncryptedSubmission(submissionName: string): Promise<EncryptedFormSubmission>;
35
+ /**
36
+ * Retrieves, decrypts and integrity-checks a submission. Note that any
37
+ * `attachments[].downloadLink` is only valid for ~10 seconds — download
38
+ * promptly (see downloadAttachment) before confirming.
39
+ */
40
+ getSubmission(submissionName: string): Promise<FormSubmission>;
41
+ /**
42
+ * Marks a submission as received. Only confirm after its answers and
43
+ * attachments are safely persisted; confirmed submissions are deleted
44
+ * from GC Forms after 30 days.
45
+ */
46
+ confirmSubmission(submissionName: string, confirmationCode: string): Promise<void>;
47
+ /** Flags a submission for GC Forms support review. */
48
+ reportProblem(submissionName: string, problem: FormSubmissionProblem): Promise<void>;
49
+ /**
50
+ * Downloads an attachment via its pre-signed link (valid ~10 seconds).
51
+ * Callers should check `attachment.isPotentiallyMalicious` before use.
52
+ */
53
+ downloadAttachment(attachment: Attachment): Promise<ArrayBuffer>;
54
+ private request;
55
+ private send;
56
+ }
57
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAIA,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/D,OAAO,EAML,KAAK,UAAU,EACf,KAAK,uBAAuB,EAC5B,KAAK,cAAc,EACnB,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACxB,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,eAAe,kDAAkD,CAAC;AAC/E,eAAO,MAAM,6BAA6B,mDACQ,CAAC;AACnD,eAAO,MAAM,0BAA0B,uBAAuB,CAAC;AAK/D,MAAM,MAAM,mBAAmB,GAAG;IAChC,0EAA0E;IAC1E,WAAW,EAAE,kBAAkB,CAAC;IAChC,6DAA6D;IAC7D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2EAA2E;IAC3E,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,oFAAoF;IACpF,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,8CAA8C;IAC9C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mDAAmD;IACnD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,YAAY,CAAC;CACtB,CAAC;AAEF,qBAAa,aAAa;IACxB,wEAAwE;IACxE,SAAgB,MAAM,EAAE,MAAM,CAAC;IAE/B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAqB;IACjD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;IACrC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgB;gBAE3B,MAAM,EAAE,mBAAmB;IAqB9C,0EAA0E;IAC7D,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAShE,8DAA8D;IACjD,iBAAiB,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC;IAYjD,sBAAsB,CACjC,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,uBAAuB,CAAC;IAYnC;;;;OAIG;IACU,aAAa,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAyB3E;;;;OAIG;IACU,iBAAiB,CAC5B,cAAc,EAAE,MAAM,EACtB,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAAC,IAAI,CAAC;IAOhB,sDAAsD;IACzC,aAAa,CACxB,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,IAAI,CAAC;IAQhB;;;OAGG;IACU,kBAAkB,CAAC,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,CAAC;YAgC/D,OAAO;YA2BP,IAAI;CA8BnB"}
package/dist/client.js ADDED
@@ -0,0 +1,149 @@
1
+ import { z } from "zod";
2
+ import { TokenProvider } from "./auth.js";
3
+ import { decryptFormSubmission, verifySubmissionIntegrity } from "./crypto.js";
4
+ import { GcFormsApiError, GcFormsDecryptionError } from "./errors.js";
5
+ import { fetchWithRetry } from "./retry.js";
6
+ import { encryptedFormSubmissionSchema, formSubmissionProblemSchema, formSubmissionSchema, newFormSubmissionSchema, gcFormsCredentialsSchema, } from "./types.js";
7
+ export const DEFAULT_API_URL = "https://api.forms-formulaires.alpha.canada.ca";
8
+ export const DEFAULT_IDENTITY_PROVIDER_URL = "https://auth.forms-formulaires.alpha.canada.ca";
9
+ export const DEFAULT_PROJECT_IDENTIFIER = "284778202772022819";
10
+ const DEFAULT_API_VERSION = "v1";
11
+ const DEFAULT_TIMEOUT_MS = 10_000;
12
+ export class GcFormsClient {
13
+ /** The form this client is bound to, taken from the private API key. */
14
+ formId;
15
+ baseUrl;
16
+ credentials;
17
+ timeoutMs;
18
+ retry;
19
+ tokenProvider;
20
+ constructor(config) {
21
+ this.credentials = gcFormsCredentialsSchema.parse(config.credentials);
22
+ this.formId = this.credentials.formId;
23
+ const apiUrl = trimTrailingSlashes(config.apiUrl ?? DEFAULT_API_URL);
24
+ const apiVersion = trimSlashes(config.apiVersion ?? DEFAULT_API_VERSION);
25
+ this.baseUrl = `${apiUrl}/${apiVersion}`;
26
+ this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
27
+ this.retry = config.retry ?? {};
28
+ this.tokenProvider = new TokenProvider({
29
+ identityProviderUrl: trimTrailingSlashes(config.identityProviderUrl ?? DEFAULT_IDENTITY_PROVIDER_URL),
30
+ projectIdentifier: config.projectIdentifier ?? DEFAULT_PROJECT_IDENTIFIER,
31
+ credentials: this.credentials,
32
+ timeoutMs: this.timeoutMs,
33
+ retry: this.retry,
34
+ });
35
+ }
36
+ /** Form structure and question definitions; shape depends on the form. */
37
+ async getFormTemplate() {
38
+ const response = await this.request("GET", `/forms/${this.formId}/template`);
39
+ return parseResponse(response, z.record(z.string(), z.unknown()), "form template");
40
+ }
41
+ /** Up to the 100 oldest submissions still in "New" status. */
42
+ async getNewSubmissions() {
43
+ const response = await this.request("GET", `/forms/${this.formId}/submission/new`);
44
+ return parseResponse(response, z.array(newFormSubmissionSchema), "new submissions");
45
+ }
46
+ async getEncryptedSubmission(submissionName) {
47
+ const response = await this.request("GET", `/forms/${this.formId}/submission/${encodeURIComponent(submissionName)}`);
48
+ return parseResponse(response, encryptedFormSubmissionSchema, `submission ${submissionName}`);
49
+ }
50
+ /**
51
+ * Retrieves, decrypts and integrity-checks a submission. Note that any
52
+ * `attachments[].downloadLink` is only valid for ~10 seconds — download
53
+ * promptly (see downloadAttachment) before confirming.
54
+ */
55
+ async getSubmission(submissionName) {
56
+ const encrypted = await this.getEncryptedSubmission(submissionName);
57
+ const decrypted = decryptFormSubmission(encrypted, this.credentials);
58
+ let submission;
59
+ try {
60
+ submission = formSubmissionSchema.parse(JSON.parse(decrypted));
61
+ }
62
+ catch (error) {
63
+ throw new GcFormsApiError(`GC Forms returned malformed decrypted data for submission ${submissionName}`, undefined, undefined, { cause: error });
64
+ }
65
+ if (!verifySubmissionIntegrity(submission.answers, submission.checksum)) {
66
+ throw new GcFormsDecryptionError(`Integrity check failed for submission ${submissionName}: answers do not match checksum`);
67
+ }
68
+ return submission;
69
+ }
70
+ /**
71
+ * Marks a submission as received. Only confirm after its answers and
72
+ * attachments are safely persisted; confirmed submissions are deleted
73
+ * from GC Forms after 30 days.
74
+ */
75
+ async confirmSubmission(submissionName, confirmationCode) {
76
+ await this.request("PUT", `/forms/${this.formId}/submission/${encodeURIComponent(submissionName)}/confirm/${encodeURIComponent(confirmationCode)}`);
77
+ }
78
+ /** Flags a submission for GC Forms support review. */
79
+ async reportProblem(submissionName, problem) {
80
+ await this.request("POST", `/forms/${this.formId}/submission/${encodeURIComponent(submissionName)}/problem`, formSubmissionProblemSchema.parse(problem));
81
+ }
82
+ /**
83
+ * Downloads an attachment via its pre-signed link (valid ~10 seconds).
84
+ * Callers should check `attachment.isPotentiallyMalicious` before use.
85
+ */
86
+ async downloadAttachment(attachment) {
87
+ let data;
88
+ let response;
89
+ try {
90
+ response = await fetchWithRetry(attachment.downloadLink, () => ({}), this.timeoutMs, this.retry, async (ok) => {
91
+ data = await ok.arrayBuffer();
92
+ });
93
+ }
94
+ catch (error) {
95
+ throw new GcFormsApiError(`Failed to download attachment '${attachment.name}'`, undefined, undefined, { cause: error });
96
+ }
97
+ if (!response.ok || data === undefined) {
98
+ throw new GcFormsApiError(`Failed to download attachment '${attachment.name}' (HTTP ${response.status})`, response.status);
99
+ }
100
+ return data;
101
+ }
102
+ async request(method, path, body) {
103
+ let response = await this.send(method, path, body);
104
+ // The cached token can be revoked server-side before its expiry; retry
105
+ // once with a freshly fetched token.
106
+ if (response.status === 401) {
107
+ await response.body?.cancel().catch(() => { });
108
+ this.tokenProvider.invalidate();
109
+ response = await this.send(method, path, body);
110
+ }
111
+ if (!response.ok) {
112
+ const responseBody = await response.text().catch(() => undefined);
113
+ throw new GcFormsApiError(`GC Forms request failed: ${method} ${path} (HTTP ${response.status})`, response.status, responseBody);
114
+ }
115
+ return response;
116
+ }
117
+ async send(method, path, body) {
118
+ const accessToken = await this.tokenProvider.getAccessToken();
119
+ try {
120
+ return await fetchWithRetry(`${this.baseUrl}${path}`, () => ({
121
+ method,
122
+ headers: {
123
+ Authorization: `Bearer ${accessToken}`,
124
+ ...(body !== undefined && { "Content-Type": "application/json" }),
125
+ },
126
+ body: body !== undefined ? JSON.stringify(body) : undefined,
127
+ }), this.timeoutMs, this.retry);
128
+ }
129
+ catch (error) {
130
+ throw new GcFormsApiError(`GC Forms request failed: ${method} ${path}`, undefined, undefined, { cause: error });
131
+ }
132
+ }
133
+ }
134
+ /** Parses a successful response body, translating malformed server data into GcFormsApiError. */
135
+ async function parseResponse(response, schema, description) {
136
+ try {
137
+ return schema.parse(await response.json());
138
+ }
139
+ catch (error) {
140
+ throw new GcFormsApiError(`GC Forms returned an unexpected response for ${description}`, undefined, undefined, { cause: error });
141
+ }
142
+ }
143
+ function trimTrailingSlashes(value) {
144
+ return value.replace(/\/+$/, "");
145
+ }
146
+ function trimSlashes(value) {
147
+ return value.replace(/^\/+|\/+$/g, "");
148
+ }
149
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { EncryptedFormSubmission, GcFormsCredentials } from "./types.js";
2
+ /**
3
+ * Decrypts a submission payload: the AES key, nonce and auth tag are each
4
+ * RSA-OAEP(SHA-256) encrypted with the form's key pair, and the responses
5
+ * themselves are AES-256-GCM encrypted. Returns the decrypted JSON string.
6
+ */
7
+ export declare function decryptFormSubmission(encryptedSubmission: EncryptedFormSubmission, credentials: GcFormsCredentials): string;
8
+ /**
9
+ * Verifies the MD5 checksum GC Forms computes over the `answers` string.
10
+ * MD5 is what the service uses; it is an integrity check, not a security
11
+ * boundary (authenticity is already covered by the GCM auth tag).
12
+ */
13
+ export declare function verifySubmissionIntegrity(answers: string, checksum: string): boolean;
14
+ //# sourceMappingURL=crypto.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,uBAAuB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAE9E;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,mBAAmB,EAAE,uBAAuB,EAC5C,WAAW,EAAE,kBAAkB,GAC9B,MAAM,CA+BR;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,OAAO,CAET"}
package/dist/crypto.js ADDED
@@ -0,0 +1,36 @@
1
+ import { createDecipheriv, createHash, privateDecrypt } from "node:crypto";
2
+ import { GcFormsDecryptionError } from "./errors.js";
3
+ /**
4
+ * Decrypts a submission payload: the AES key, nonce and auth tag are each
5
+ * RSA-OAEP(SHA-256) encrypted with the form's key pair, and the responses
6
+ * themselves are AES-256-GCM encrypted. Returns the decrypted JSON string.
7
+ */
8
+ export function decryptFormSubmission(encryptedSubmission, credentials) {
9
+ try {
10
+ const privateKey = { key: credentials.key, oaepHash: "sha256" };
11
+ const key = privateDecrypt(privateKey, Buffer.from(encryptedSubmission.encryptedKey, "base64"));
12
+ const nonce = privateDecrypt(privateKey, Buffer.from(encryptedSubmission.encryptedNonce, "base64"));
13
+ const authTag = privateDecrypt(privateKey, Buffer.from(encryptedSubmission.encryptedAuthTag, "base64"));
14
+ const decipher = createDecipheriv("aes-256-gcm", key, nonce);
15
+ decipher.setAuthTag(authTag);
16
+ const decrypted = Buffer.concat([
17
+ decipher.update(Buffer.from(encryptedSubmission.encryptedResponses, "base64")),
18
+ decipher.final(),
19
+ ]);
20
+ return decrypted.toString("utf8");
21
+ }
22
+ catch (error) {
23
+ throw new GcFormsDecryptionError("Failed to decrypt form submission", {
24
+ cause: error,
25
+ });
26
+ }
27
+ }
28
+ /**
29
+ * Verifies the MD5 checksum GC Forms computes over the `answers` string.
30
+ * MD5 is what the service uses; it is an integrity check, not a security
31
+ * boundary (authenticity is already covered by the GCM auth tag).
32
+ */
33
+ export function verifySubmissionIntegrity(answers, checksum) {
34
+ return createHash("md5").update(answers).digest("hex") === checksum;
35
+ }
36
+ //# sourceMappingURL=crypto.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.js","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC3E,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAGrD;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CACnC,mBAA4C,EAC5C,WAA+B;IAE/B,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,EAAE,GAAG,EAAE,WAAW,CAAC,GAAG,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC;QAEhE,MAAM,GAAG,GAAG,cAAc,CACxB,UAAU,EACV,MAAM,CAAC,IAAI,CAAC,mBAAmB,CAAC,YAAY,EAAE,QAAQ,CAAC,CACxD,CAAC;QACF,MAAM,KAAK,GAAG,cAAc,CAC1B,UAAU,EACV,MAAM,CAAC,IAAI,CAAC,mBAAmB,CAAC,cAAc,EAAE,QAAQ,CAAC,CAC1D,CAAC;QACF,MAAM,OAAO,GAAG,cAAc,CAC5B,UAAU,EACV,MAAM,CAAC,IAAI,CAAC,mBAAmB,CAAC,gBAAgB,EAAE,QAAQ,CAAC,CAC5D,CAAC;QAEF,MAAM,QAAQ,GAAG,gBAAgB,CAAC,aAAa,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;QAC7D,QAAQ,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;QAE7B,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC;YAC9B,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,mBAAmB,CAAC,kBAAkB,EAAE,QAAQ,CAAC,CAAC;YAC9E,QAAQ,CAAC,KAAK,EAAE;SACjB,CAAC,CAAC;QAEH,OAAO,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACpC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,sBAAsB,CAAC,mCAAmC,EAAE;YACpE,KAAK,EAAE,KAAK;SACb,CAAC,CAAC;IACL,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,yBAAyB,CACvC,OAAe,EACf,QAAgB;IAEhB,OAAO,UAAU,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,QAAQ,CAAC;AACtE,CAAC"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Base class for every error thrown by this package; `instanceof
3
+ * GcFormsApiError` catches them all.
4
+ */
5
+ export declare class GcFormsApiError extends Error {
6
+ readonly status?: number | undefined;
7
+ readonly responseBody?: string | undefined;
8
+ constructor(message: string, status?: number | undefined, responseBody?: string | undefined, options?: ErrorOptions);
9
+ }
10
+ /** Failure to obtain an access token from the GC Forms identity provider. */
11
+ export declare class GcFormsAuthError extends GcFormsApiError {
12
+ constructor(message: string, status?: number, responseBody?: string, options?: ErrorOptions);
13
+ }
14
+ /** Failure to decrypt a submission or verify its integrity checksum. */
15
+ export declare class GcFormsDecryptionError extends GcFormsApiError {
16
+ constructor(message: string, options?: ErrorOptions);
17
+ }
18
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,qBAAa,eAAgB,SAAQ,KAAK;aAGtB,MAAM,CAAC,EAAE,MAAM;aACf,YAAY,CAAC,EAAE,MAAM;gBAFrC,OAAO,EAAE,MAAM,EACC,MAAM,CAAC,EAAE,MAAM,YAAA,EACf,YAAY,CAAC,EAAE,MAAM,YAAA,EACrC,OAAO,CAAC,EAAE,YAAY;CAKzB;AAED,6EAA6E;AAC7E,qBAAa,gBAAiB,SAAQ,eAAe;gBAEjD,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,MAAM,EACf,YAAY,CAAC,EAAE,MAAM,EACrB,OAAO,CAAC,EAAE,YAAY;CAKzB;AAED,wEAAwE;AACxE,qBAAa,sBAAuB,SAAQ,eAAe;gBACtC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY;CAI3D"}
package/dist/errors.js ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Base class for every error thrown by this package; `instanceof
3
+ * GcFormsApiError` catches them all.
4
+ */
5
+ export class GcFormsApiError extends Error {
6
+ status;
7
+ responseBody;
8
+ constructor(message, status, responseBody, options) {
9
+ super(message, options);
10
+ this.status = status;
11
+ this.responseBody = responseBody;
12
+ this.name = "GcFormsApiError";
13
+ }
14
+ }
15
+ /** Failure to obtain an access token from the GC Forms identity provider. */
16
+ export class GcFormsAuthError extends GcFormsApiError {
17
+ constructor(message, status, responseBody, options) {
18
+ super(message, status, responseBody, options);
19
+ this.name = "GcFormsAuthError";
20
+ }
21
+ }
22
+ /** Failure to decrypt a submission or verify its integrity checksum. */
23
+ export class GcFormsDecryptionError extends GcFormsApiError {
24
+ constructor(message, options) {
25
+ super(message, undefined, undefined, options);
26
+ this.name = "GcFormsDecryptionError";
27
+ }
28
+ }
29
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,OAAO,eAAgB,SAAQ,KAAK;IAGtB;IACA;IAHlB,YACE,OAAe,EACC,MAAe,EACf,YAAqB,EACrC,OAAsB;QAEtB,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAJR,WAAM,GAAN,MAAM,CAAS;QACf,iBAAY,GAAZ,YAAY,CAAS;QAIrC,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;IAChC,CAAC;CACF;AAED,6EAA6E;AAC7E,MAAM,OAAO,gBAAiB,SAAQ,eAAe;IACnD,YACE,OAAe,EACf,MAAe,EACf,YAAqB,EACrB,OAAsB;QAEtB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,CAAC,CAAC;QAC9C,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;IACjC,CAAC;CACF;AAED,wEAAwE;AACxE,MAAM,OAAO,sBAAuB,SAAQ,eAAe;IACzD,YAAmB,OAAe,EAAE,OAAsB;QACxD,KAAK,CAAC,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;QAC9C,IAAI,CAAC,IAAI,GAAG,wBAAwB,CAAC;IACvC,CAAC;CACF"}
@@ -0,0 +1,7 @@
1
+ export { GcFormsClient, DEFAULT_API_URL, DEFAULT_IDENTITY_PROVIDER_URL, DEFAULT_PROJECT_IDENTIFIER, } from "./client.js";
2
+ export type { GcFormsClientConfig } from "./client.js";
3
+ export { GcFormsApiError, GcFormsAuthError, GcFormsDecryptionError, } from "./errors.js";
4
+ export type { RetryOptions } from "./retry.js";
5
+ export { decryptFormSubmission, verifySubmissionIntegrity } from "./crypto.js";
6
+ export * from "./types.js";
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,eAAe,EACf,6BAA6B,EAC7B,0BAA0B,GAC3B,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,sBAAsB,GACvB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,qBAAqB,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AAC/E,cAAc,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { GcFormsClient, DEFAULT_API_URL, DEFAULT_IDENTITY_PROVIDER_URL, DEFAULT_PROJECT_IDENTIFIER, } from "./client.js";
2
+ export { GcFormsApiError, GcFormsAuthError, GcFormsDecryptionError, } from "./errors.js";
3
+ export { decryptFormSubmission, verifySubmissionIntegrity } from "./crypto.js";
4
+ export * from "./types.js";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,eAAe,EACf,6BAA6B,EAC7B,0BAA0B,GAC3B,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,sBAAsB,GACvB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,qBAAqB,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AAC/E,cAAc,YAAY,CAAC"}
@@ -0,0 +1,23 @@
1
+ export type RetryOptions = {
2
+ /** Total number of attempts, including the first. Set to 1 to disable retries. */
3
+ maxAttempts?: number;
4
+ /** Base for the exponential backoff between attempts. */
5
+ baseDelayMs?: number;
6
+ /** Upper bound on the backoff between attempts. */
7
+ maxDelayMs?: number;
8
+ /** HTTP status codes that trigger a retry. */
9
+ retryStatuses?: number[];
10
+ };
11
+ /**
12
+ * fetch with a per-attempt timeout and exponential backoff (full jitter) on
13
+ * retryable statuses and network errors. `makeInit` is called once per
14
+ * attempt so time-sensitive request material (e.g. a signed JWT assertion)
15
+ * is rebuilt fresh. Returns the last response, retryable or not; throws only
16
+ * when the final attempt fails at the network level.
17
+ *
18
+ * `consumeBody`, when given, runs for each ok response inside the attempt,
19
+ * so the per-attempt timeout and retry policy cover the body download too —
20
+ * a body read aborted by the timeout retries like any network error.
21
+ */
22
+ export declare function fetchWithRetry(url: string, makeInit: () => RequestInit, timeoutMs: number, { maxAttempts, baseDelayMs, maxDelayMs, retryStatuses, }?: RetryOptions, consumeBody?: (response: Response) => Promise<void>): Promise<Response>;
23
+ //# sourceMappingURL=retry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../src/retry.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG;IACzB,kFAAkF;IAClF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yDAAyD;IACzD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mDAAmD;IACnD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8CAA8C;IAC9C,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B,CAAC;AAKF;;;;;;;;;;GAUG;AACH,wBAAsB,cAAc,CAClC,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,WAAW,EAC3B,SAAS,EAAE,MAAM,EACjB,EACE,WAAe,EACf,WAAiB,EACjB,UAAkB,EAClB,aAAyC,GAC1C,GAAE,YAAiB,EACpB,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,KAAK,OAAO,CAAC,IAAI,CAAC,GAClD,OAAO,CAAC,QAAQ,CAAC,CA6BnB"}
package/dist/retry.js ADDED
@@ -0,0 +1,60 @@
1
+ /** Never wait longer than this between attempts, even if Retry-After asks for more. */
2
+ const RETRY_AFTER_CAP_MS = 30_000;
3
+ /**
4
+ * fetch with a per-attempt timeout and exponential backoff (full jitter) on
5
+ * retryable statuses and network errors. `makeInit` is called once per
6
+ * attempt so time-sensitive request material (e.g. a signed JWT assertion)
7
+ * is rebuilt fresh. Returns the last response, retryable or not; throws only
8
+ * when the final attempt fails at the network level.
9
+ *
10
+ * `consumeBody`, when given, runs for each ok response inside the attempt,
11
+ * so the per-attempt timeout and retry policy cover the body download too —
12
+ * a body read aborted by the timeout retries like any network error.
13
+ */
14
+ export async function fetchWithRetry(url, makeInit, timeoutMs, { maxAttempts = 3, baseDelayMs = 250, maxDelayMs = 8_000, retryStatuses = [429, 500, 502, 503, 504], } = {}, consumeBody) {
15
+ const backoffDelayMs = (attempt) => Math.random() * Math.min(maxDelayMs, baseDelayMs * 2 ** attempt);
16
+ for (let attempt = 0;; attempt++) {
17
+ const isLastAttempt = attempt >= maxAttempts - 1;
18
+ let response;
19
+ try {
20
+ response = await fetch(url, {
21
+ ...makeInit(),
22
+ signal: AbortSignal.timeout(timeoutMs),
23
+ });
24
+ if (response.ok && consumeBody) {
25
+ await consumeBody(response);
26
+ }
27
+ }
28
+ catch (error) {
29
+ if (isLastAttempt)
30
+ throw error;
31
+ await sleep(backoffDelayMs(attempt));
32
+ continue;
33
+ }
34
+ if (isLastAttempt || !retryStatuses.includes(response.status)) {
35
+ return response;
36
+ }
37
+ await response.body?.cancel().catch(() => { });
38
+ await sleep(retryAfterMs(response) ?? backoffDelayMs(attempt));
39
+ }
40
+ }
41
+ function retryAfterMs(response) {
42
+ const header = response.headers.get("retry-after");
43
+ // Treat an empty value as absent: Number("") is 0, which would turn a
44
+ // buggy proxy's bare header into zero-delay retries with no backoff.
45
+ if (header === null || header.trim() === "")
46
+ return undefined;
47
+ const seconds = Number(header);
48
+ if (Number.isFinite(seconds) && seconds >= 0) {
49
+ return Math.min(seconds * 1000, RETRY_AFTER_CAP_MS);
50
+ }
51
+ const date = Date.parse(header);
52
+ if (!Number.isNaN(date)) {
53
+ return Math.min(Math.max(0, date - Date.now()), RETRY_AFTER_CAP_MS);
54
+ }
55
+ return undefined;
56
+ }
57
+ function sleep(ms) {
58
+ return new Promise((resolve) => setTimeout(resolve, ms));
59
+ }
60
+ //# sourceMappingURL=retry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry.js","sourceRoot":"","sources":["../src/retry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,67 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Contents of the `<formId>_private_api_key.json` file generated from
4
+ * GC Forms under "Settings > API integration". `key` is a PEM-encoded
5
+ * RSA private key used both to sign the OAuth JWT assertion and to
6
+ * decrypt submissions.
7
+ */
8
+ export declare const gcFormsCredentialsSchema: z.ZodObject<{
9
+ keyId: z.ZodString;
10
+ key: z.ZodString;
11
+ userId: z.ZodString;
12
+ formId: z.ZodString;
13
+ }, z.core.$strip>;
14
+ export type GcFormsCredentials = z.infer<typeof gcFormsCredentialsSchema>;
15
+ export declare const newFormSubmissionSchema: z.ZodObject<{
16
+ name: z.ZodString;
17
+ createdAt: z.ZodNumber;
18
+ }, z.core.$strip>;
19
+ export type NewFormSubmission = z.infer<typeof newFormSubmissionSchema>;
20
+ export declare const encryptedFormSubmissionSchema: z.ZodObject<{
21
+ encryptedKey: z.ZodString;
22
+ encryptedNonce: z.ZodString;
23
+ encryptedAuthTag: z.ZodString;
24
+ encryptedResponses: z.ZodString;
25
+ }, z.core.$strip>;
26
+ export type EncryptedFormSubmission = z.infer<typeof encryptedFormSubmissionSchema>;
27
+ export declare const formSubmissionStatusSchema: z.ZodEnum<{
28
+ New: "New";
29
+ Downloaded: "Downloaded";
30
+ Confirmed: "Confirmed";
31
+ Problem: "Problem";
32
+ }>;
33
+ export type FormSubmissionStatus = z.infer<typeof formSubmissionStatusSchema>;
34
+ export declare const attachmentSchema: z.ZodObject<{
35
+ name: z.ZodString;
36
+ downloadLink: z.ZodString;
37
+ isPotentiallyMalicious: z.ZodBoolean;
38
+ }, z.core.$strip>;
39
+ export type Attachment = z.infer<typeof attachmentSchema>;
40
+ export declare const formSubmissionSchema: z.ZodObject<{
41
+ createdAt: z.ZodNumber;
42
+ status: z.ZodEnum<{
43
+ New: "New";
44
+ Downloaded: "Downloaded";
45
+ Confirmed: "Confirmed";
46
+ Problem: "Problem";
47
+ }>;
48
+ confirmationCode: z.ZodString;
49
+ answers: z.ZodString;
50
+ checksum: z.ZodString;
51
+ attachments: z.ZodOptional<z.ZodArray<z.ZodObject<{
52
+ name: z.ZodString;
53
+ downloadLink: z.ZodString;
54
+ isPotentiallyMalicious: z.ZodBoolean;
55
+ }, z.core.$strip>>>;
56
+ }, z.core.$strip>;
57
+ export type FormSubmission = z.infer<typeof formSubmissionSchema>;
58
+ export declare const formSubmissionProblemSchema: z.ZodObject<{
59
+ contactEmail: z.ZodEmail;
60
+ description: z.ZodString;
61
+ preferredLanguage: z.ZodEnum<{
62
+ en: "en";
63
+ fr: "fr";
64
+ }>;
65
+ }, z.core.$strip>;
66
+ export type FormSubmissionProblem = z.infer<typeof formSubmissionProblemSchema>;
67
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB;;;;;iBAKnC,CAAC;AAEH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAE1E,eAAO,MAAM,uBAAuB;;;iBAGlC,CAAC;AAEH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAExE,eAAO,MAAM,6BAA6B;;;;;iBAKxC,CAAC;AAEH,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAC3C,OAAO,6BAA6B,CACrC,CAAC;AAEF,eAAO,MAAM,0BAA0B;;;;;EAKrC,CAAC;AAEH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAE9E,eAAO,MAAM,gBAAgB;;;;iBAK3B,CAAC;AAEH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;iBAQ/B,CAAC;AAEH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE,eAAO,MAAM,2BAA2B;;;;;;;iBAItC,CAAC;AAEH,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,2BAA2B,CAAC,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1,50 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Contents of the `<formId>_private_api_key.json` file generated from
4
+ * GC Forms under "Settings > API integration". `key` is a PEM-encoded
5
+ * RSA private key used both to sign the OAuth JWT assertion and to
6
+ * decrypt submissions.
7
+ */
8
+ export const gcFormsCredentialsSchema = z.object({
9
+ keyId: z.string().min(1),
10
+ key: z.string().min(1),
11
+ userId: z.string().min(1),
12
+ formId: z.string().min(1),
13
+ });
14
+ export const newFormSubmissionSchema = z.object({
15
+ name: z.string().min(1),
16
+ createdAt: z.number(),
17
+ });
18
+ export const encryptedFormSubmissionSchema = z.object({
19
+ encryptedKey: z.string().min(1),
20
+ encryptedNonce: z.string().min(1),
21
+ encryptedAuthTag: z.string().min(1),
22
+ encryptedResponses: z.string().min(1),
23
+ });
24
+ export const formSubmissionStatusSchema = z.enum([
25
+ "New",
26
+ "Downloaded",
27
+ "Confirmed",
28
+ "Problem",
29
+ ]);
30
+ export const attachmentSchema = z.object({
31
+ name: z.string(),
32
+ /** Pre-signed URL, only valid for ~10 seconds after retrieval. */
33
+ downloadLink: z.string(),
34
+ isPotentiallyMalicious: z.boolean(),
35
+ });
36
+ export const formSubmissionSchema = z.object({
37
+ createdAt: z.number(),
38
+ status: formSubmissionStatusSchema,
39
+ confirmationCode: z.string(),
40
+ /** JSON string mapping question ids to answers; shape depends on the form template. */
41
+ answers: z.string(),
42
+ checksum: z.string(),
43
+ attachments: z.array(attachmentSchema).optional(),
44
+ });
45
+ export const formSubmissionProblemSchema = z.object({
46
+ contactEmail: z.email(),
47
+ description: z.string().min(10),
48
+ preferredLanguage: z.enum(["en", "fr"]),
49
+ });
50
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;GAKG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC/C,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACxB,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACtB,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACzB,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CAC1B,CAAC,CAAC;AAIH,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9C,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACvB,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;CACtB,CAAC,CAAC;AAIH,MAAM,CAAC,MAAM,6BAA6B,GAAG,CAAC,CAAC,MAAM,CAAC;IACpD,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC/B,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACjC,gBAAgB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACnC,kBAAkB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CACtC,CAAC,CAAC;AAMH,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,CAAC,IAAI,CAAC;IAC/C,KAAK;IACL,YAAY;IACZ,WAAW;IACX,SAAS;CACV,CAAC,CAAC;AAIH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC;IACvC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,kEAAkE;IAClE,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE;IACxB,sBAAsB,EAAE,CAAC,CAAC,OAAO,EAAE;CACpC,CAAC,CAAC;AAIH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3C,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;IACrB,MAAM,EAAE,0BAA0B;IAClC,gBAAgB,EAAE,CAAC,CAAC,MAAM,EAAE;IAC5B,uFAAuF;IACvF,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;IACnB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;IACpB,WAAW,EAAE,CAAC,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC,QAAQ,EAAE;CAClD,CAAC,CAAC;AAIH,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,CAAC,MAAM,CAAC;IAClD,YAAY,EAAE,CAAC,CAAC,KAAK,EAAE;IACvB,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC;IAC/B,iBAAiB,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;CACxC,CAAC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@paper19/gcforms-client",
3
+ "version": "1.0.0",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "description": "Unofficial TypeScript client for the Government of Canada Forms API (auth, submission decryption, retries)",
8
+ "license": "MIT",
9
+ "type": "module",
10
+ "engines": {
11
+ "node": ">=20"
12
+ },
13
+ "main": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js",
19
+ "default": "./dist/index.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "README.md",
25
+ "LICENSE"
26
+ ],
27
+ "sideEffects": false,
28
+ "keywords": [
29
+ "gcforms",
30
+ "gc-forms",
31
+ "canada",
32
+ "forms",
33
+ "api-client"
34
+ ],
35
+ "scripts": {
36
+ "build": "npm run clean && tsc -p tsconfig.build.json",
37
+ "clean": "rm -rf dist",
38
+ "typecheck": "tsc -p tsconfig.json",
39
+ "test": "vitest run",
40
+ "test:watch": "vitest",
41
+ "prepack": "npm run build"
42
+ },
43
+ "dependencies": {
44
+ "zod": "^4.4.3"
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^20",
48
+ "typescript": "^5",
49
+ "vitest": "^3"
50
+ }
51
+ }