@layrstack/public-client 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -2
- package/dist/angular/index.d.ts +47 -0
- package/dist/angular/index.d.ts.map +1 -0
- package/dist/angular/index.js +80 -0
- package/dist/angular/index.js.map +1 -0
- package/dist/captcha.d.ts +3 -0
- package/dist/captcha.d.ts.map +1 -0
- package/dist/captcha.js +33 -0
- package/dist/captcha.js.map +1 -0
- package/dist/client.d.ts +32 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +205 -0
- package/dist/client.js.map +1 -0
- package/dist/errors.d.ts +36 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +29 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/transport.d.ts +9 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +43 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +97 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/validate.d.ts +10 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +52 -0
- package/dist/validate.js.map +1 -0
- package/package.json +68 -4
package/README.md
CHANGED
|
@@ -1,3 +1,74 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @layrstack/public-client
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Submit a LayrStack **public form** that has file fields with **one call**. The client does the whole flow:
|
|
4
|
+
|
|
5
|
+
1. loads the form's rules (`GET /public/:tenant/views/:view`) and checks the chosen files against them
|
|
6
|
+
2. for each file: **upload intent** → **PUT to the storage URL** (with progress) → **complete**
|
|
7
|
+
3. gets a fresh **reCAPTCHA** token (when the form requires it)
|
|
8
|
+
4. **submits** the record with the file ids
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { createPublicFormClient } from '@layrstack/public-client';
|
|
12
|
+
|
|
13
|
+
const forms = createPublicFormClient({ apiBase: 'https://acme.api.example.com/api', tenant: 'acme' });
|
|
14
|
+
|
|
15
|
+
const record = await forms.submit('contact-form', {
|
|
16
|
+
values: { name: 'Asha', message: 'Hello' }, // every non-file field
|
|
17
|
+
files: { photo: photoFile, documents: [a, b] }, // File or File[] per file field
|
|
18
|
+
onProgress: (p) => console.log(p.stage, p.percent, p.files),
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Angular
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
// app.config.ts
|
|
26
|
+
import { providePublicForm } from '@layrstack/public-client/angular';
|
|
27
|
+
export const appConfig = { providers: [providePublicForm({ apiBase: 'https://acme.api.example.com/api', tenant: 'acme' })] };
|
|
28
|
+
|
|
29
|
+
// component
|
|
30
|
+
readonly submission = inject(PublicFormService).createSubmission('contact-form');
|
|
31
|
+
await this.submission.submit({ values, files });
|
|
32
|
+
// template: submission.progress(), submission.submitting(), submission.error(), submission.record()
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`PublicFormService` also has `submit()` (a Promise), `submit$()` (an Observable of `progress` events then `done`; unsubscribing cancels) and `getContract()`. See `samples/contact-form.component.ts`.
|
|
36
|
+
The Angular entry point needs `@angular/core` and `rxjs` (optional peer dependencies); the core has no dependencies.
|
|
37
|
+
|
|
38
|
+
## reCAPTCHA
|
|
39
|
+
|
|
40
|
+
When the form requires it, the client takes the site key from the form (`recaptchaSiteKey`), loads Google's script, and requests a token with the action `public_submit` **after the uploads**, right before the submit (tokens are single-use and live about two minutes). Uploads send no captcha token. To use your own mechanism, pass `getCaptchaToken(siteKey)`.
|
|
41
|
+
|
|
42
|
+
## Errors
|
|
43
|
+
|
|
44
|
+
Every failure is a `PublicFormError`:
|
|
45
|
+
|
|
46
|
+
| field | meaning |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| `stage` | `contract`, `validate`, `upload`, `captcha`, `submit` or `aborted` |
|
|
49
|
+
| `message` | the server's message when there is one |
|
|
50
|
+
| `field`, `fileName` | which field / file failed (validation and upload) |
|
|
51
|
+
| `status` | HTTP status of the failed call |
|
|
52
|
+
| `retryable` | calling `submit` again is likely to work |
|
|
53
|
+
| `ambiguous` | the final submit may have reached the server; check before resending |
|
|
54
|
+
|
|
55
|
+
**Retrying is safe**: call `submit` again with the *same `File` objects*; files that already uploaded are not uploaded again. The final submit is never retried automatically (it would risk a duplicate record). Failed storage uploads are retried up to `retries` times (default 2) with a new upload intent.
|
|
56
|
+
|
|
57
|
+
## Options
|
|
58
|
+
|
|
59
|
+
`apiBase` (with `/api`, on the tenant host) · `tenant` · `token` (token-mode views only) · `concurrency` (3) · `retries` (2) · `retryDelayMs` (500) · `recaptchaSiteKey` · `getCaptchaToken` · `fetch` · `putFile`.
|
|
60
|
+
|
|
61
|
+
## Set up your site's origin (required once)
|
|
62
|
+
|
|
63
|
+
- **API CORS**: add the site's origin to `FRONTEND_ORIGIN` on the backend.
|
|
64
|
+
- **Storage CORS**: the bucket must allow `PUT` from the site's origin, with the `Content-Type` header.
|
|
65
|
+
- **Do not use token-mode views** from a public website: a token in browser code is visible to everyone. Use a public view with reCAPTCHA.
|
|
66
|
+
|
|
67
|
+
## Develop
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pnpm --filter @layrstack/public-client test # unit tests (fake API and storage)
|
|
71
|
+
pnpm --filter @layrstack/public-client harness # http://localhost:4300, runs the flow against your local backend
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The harness origin (`http://localhost:4300`) must be in `FRONTEND_ORIGIN`, and the bucket's CORS must allow it.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { InjectionToken, type Provider } from '@angular/core';
|
|
2
|
+
import { Observable } from 'rxjs';
|
|
3
|
+
import { PublicFormClient, PublicFormError } from '../index.js';
|
|
4
|
+
import type { PublicFormConfig, PublicRecord, PublicViewContract, SubmitInput, SubmitProgress } from '../index.js';
|
|
5
|
+
export { PublicFormError } from '../index.js';
|
|
6
|
+
export type { FileProgress, PublicFormConfig, PublicRecord, PublicViewContract, SubmitInput, SubmitProgress } from '../index.js';
|
|
7
|
+
export declare const PUBLIC_FORM_CONFIG: InjectionToken<PublicFormConfig>;
|
|
8
|
+
export type PublicFormEvent = {
|
|
9
|
+
type: 'progress';
|
|
10
|
+
progress: SubmitProgress;
|
|
11
|
+
} | {
|
|
12
|
+
type: 'done';
|
|
13
|
+
record: PublicRecord;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Injectable entry point: `inject(PublicFormService)` after `providePublicForm({ apiBase, tenant })`.
|
|
17
|
+
* (It is built by a factory provider, so it needs no Angular compiler step and works with any Angular 17+ app.)
|
|
18
|
+
*/
|
|
19
|
+
export declare class PublicFormService {
|
|
20
|
+
readonly client: PublicFormClient;
|
|
21
|
+
constructor(config: PublicFormConfig);
|
|
22
|
+
getContract(view: string, options?: {
|
|
23
|
+
refresh?: boolean;
|
|
24
|
+
}): Promise<PublicViewContract>;
|
|
25
|
+
/** Uploads the files and submits the record; resolves with the created record. */
|
|
26
|
+
submit(view: string, input: SubmitInput): Promise<PublicRecord>;
|
|
27
|
+
/** The same as a stream: `progress` events, then one `done` event. Unsubscribing cancels the uploads. */
|
|
28
|
+
submit$(view: string, input: Omit<SubmitInput, 'onProgress' | 'signal'>): Observable<PublicFormEvent>;
|
|
29
|
+
/** Signal-based state for one form: progress, submitting, error, and the created record. */
|
|
30
|
+
createSubmission(view: string): PublicFormSubmission;
|
|
31
|
+
}
|
|
32
|
+
/** Submit state as signals, ready for a template. `submit()` never throws: failures land in `error`. */
|
|
33
|
+
export declare class PublicFormSubmission {
|
|
34
|
+
private readonly client;
|
|
35
|
+
private readonly view;
|
|
36
|
+
readonly progress: import("@angular/core").WritableSignal<SubmitProgress | null>;
|
|
37
|
+
readonly submitting: import("@angular/core").WritableSignal<boolean>;
|
|
38
|
+
readonly error: import("@angular/core").WritableSignal<PublicFormError | null>;
|
|
39
|
+
readonly record: import("@angular/core").WritableSignal<PublicRecord | null>;
|
|
40
|
+
private controller;
|
|
41
|
+
constructor(client: PublicFormClient, view: string);
|
|
42
|
+
submit(input: Omit<SubmitInput, 'onProgress' | 'signal'>): Promise<PublicRecord | null>;
|
|
43
|
+
cancel(): void;
|
|
44
|
+
}
|
|
45
|
+
/** `providers: [providePublicForm({ apiBase: 'https://acme.api.example.com/api', tenant: 'acme' })]` */
|
|
46
|
+
export declare function providePublicForm(config: PublicFormConfig): Provider[];
|
|
47
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/angular/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAU,KAAK,QAAQ,EAAE,MAAM,eAAe,CAAC;AACtE,OAAO,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAClC,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAChE,OAAO,KAAK,EAAE,gBAAgB,EAAE,YAAY,EAAE,kBAAkB,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAEnH,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,YAAY,EAAE,YAAY,EAAE,gBAAgB,EAAE,YAAY,EAAE,kBAAkB,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAEjI,eAAO,MAAM,kBAAkB,kCAAuE,CAAC;AAEvG,MAAM,MAAM,eAAe,GAAG;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE,cAAc,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,YAAY,CAAA;CAAE,CAAC;AAEtH;;;GAGG;AACH,qBAAa,iBAAiB;IAC5B,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;gBAEtB,MAAM,EAAE,gBAAgB;IAIpC,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAIvF,kFAAkF;IAClF,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;IAI/D,yGAAyG;IACzG,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,YAAY,GAAG,QAAQ,CAAC,GAAG,UAAU,CAAC,eAAe,CAAC;IAWrG,4FAA4F;IAC5F,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,oBAAoB;CAGrD;AAED,wGAAwG;AACxG,qBAAa,oBAAoB;IAOnB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAAoB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAN5E,QAAQ,CAAC,QAAQ,gEAAuC;IACxD,QAAQ,CAAC,UAAU,kDAAiB;IACpC,QAAQ,CAAC,KAAK,iEAAwC;IACtD,QAAQ,CAAC,MAAM,8DAAqC;IACpD,OAAO,CAAC,UAAU,CAAgC;gBAErB,MAAM,EAAE,gBAAgB,EAAmB,IAAI,EAAE,MAAM;IAE9E,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,YAAY,GAAG,QAAQ,CAAC,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IAmB7F,MAAM,IAAI,IAAI;CAGf;AAED,wGAAwG;AACxG,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,gBAAgB,GAAG,QAAQ,EAAE,CAKtE"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { InjectionToken, signal } from '@angular/core';
|
|
2
|
+
import { Observable } from 'rxjs';
|
|
3
|
+
import { PublicFormClient, PublicFormError } from '../index.js';
|
|
4
|
+
export { PublicFormError } from '../index.js';
|
|
5
|
+
export const PUBLIC_FORM_CONFIG = new InjectionToken('LAYRSTACK_PUBLIC_FORM_CONFIG');
|
|
6
|
+
/**
|
|
7
|
+
* Injectable entry point: `inject(PublicFormService)` after `providePublicForm({ apiBase, tenant })`.
|
|
8
|
+
* (It is built by a factory provider, so it needs no Angular compiler step and works with any Angular 17+ app.)
|
|
9
|
+
*/
|
|
10
|
+
export class PublicFormService {
|
|
11
|
+
client;
|
|
12
|
+
constructor(config) {
|
|
13
|
+
this.client = new PublicFormClient(config);
|
|
14
|
+
}
|
|
15
|
+
getContract(view, options) {
|
|
16
|
+
return this.client.getContract(view, options);
|
|
17
|
+
}
|
|
18
|
+
/** Uploads the files and submits the record; resolves with the created record. */
|
|
19
|
+
submit(view, input) {
|
|
20
|
+
return this.client.submit(view, input);
|
|
21
|
+
}
|
|
22
|
+
/** The same as a stream: `progress` events, then one `done` event. Unsubscribing cancels the uploads. */
|
|
23
|
+
submit$(view, input) {
|
|
24
|
+
return new Observable((subscriber) => {
|
|
25
|
+
const controller = new AbortController();
|
|
26
|
+
this.client.submit(view, { ...input, signal: controller.signal, onProgress: (progress) => subscriber.next({ type: 'progress', progress }) }).then((record) => { subscriber.next({ type: 'done', record }); subscriber.complete(); }, (error) => subscriber.error(error));
|
|
27
|
+
return () => controller.abort();
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/** Signal-based state for one form: progress, submitting, error, and the created record. */
|
|
31
|
+
createSubmission(view) {
|
|
32
|
+
return new PublicFormSubmission(this.client, view);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Submit state as signals, ready for a template. `submit()` never throws: failures land in `error`. */
|
|
36
|
+
export class PublicFormSubmission {
|
|
37
|
+
client;
|
|
38
|
+
view;
|
|
39
|
+
progress = signal(null);
|
|
40
|
+
submitting = signal(false);
|
|
41
|
+
error = signal(null);
|
|
42
|
+
record = signal(null);
|
|
43
|
+
controller = null;
|
|
44
|
+
constructor(client, view) {
|
|
45
|
+
this.client = client;
|
|
46
|
+
this.view = view;
|
|
47
|
+
}
|
|
48
|
+
async submit(input) {
|
|
49
|
+
if (this.submitting())
|
|
50
|
+
return null;
|
|
51
|
+
this.controller = new AbortController();
|
|
52
|
+
this.submitting.set(true);
|
|
53
|
+
this.error.set(null);
|
|
54
|
+
this.record.set(null);
|
|
55
|
+
try {
|
|
56
|
+
const record = await this.client.submit(this.view, { ...input, signal: this.controller.signal, onProgress: (progress) => this.progress.set(progress) });
|
|
57
|
+
this.record.set(record);
|
|
58
|
+
return record;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
this.error.set(error instanceof PublicFormError ? error : new PublicFormError(error instanceof Error ? error.message : 'The form could not be submitted.', 'submit', { cause: error }));
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
this.submitting.set(false);
|
|
66
|
+
this.controller = null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
cancel() {
|
|
70
|
+
this.controller?.abort();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** `providers: [providePublicForm({ apiBase: 'https://acme.api.example.com/api', tenant: 'acme' })]` */
|
|
74
|
+
export function providePublicForm(config) {
|
|
75
|
+
return [
|
|
76
|
+
{ provide: PUBLIC_FORM_CONFIG, useValue: config },
|
|
77
|
+
{ provide: PublicFormService, useFactory: (value) => new PublicFormService(value), deps: [PUBLIC_FORM_CONFIG] },
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/angular/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,EAAiB,MAAM,eAAe,CAAC;AACtE,OAAO,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAClC,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAGhE,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAG9C,MAAM,CAAC,MAAM,kBAAkB,GAAG,IAAI,cAAc,CAAmB,8BAA8B,CAAC,CAAC;AAIvG;;;GAGG;AACH,MAAM,OAAO,iBAAiB;IACnB,MAAM,CAAmB;IAElC,YAAY,MAAwB;QAClC,IAAI,CAAC,MAAM,GAAG,IAAI,gBAAgB,CAAC,MAAM,CAAC,CAAC;IAC7C,CAAC;IAED,WAAW,CAAC,IAAY,EAAE,OAA+B;QACvD,OAAO,IAAI,CAAC,MAAM,CAAC,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAChD,CAAC;IAED,kFAAkF;IAClF,MAAM,CAAC,IAAY,EAAE,KAAkB;QACrC,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACzC,CAAC;IAED,yGAAyG;IACzG,OAAO,CAAC,IAAY,EAAE,KAAiD;QACrE,OAAO,IAAI,UAAU,CAAkB,CAAC,UAAU,EAAE,EAAE;YACpD,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;YACzC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,GAAG,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,UAAU,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAC,IAAI,CAC/I,CAAC,MAAM,EAAE,EAAE,GAAG,UAAU,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EACjF,CAAC,KAAK,EAAE,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,CACnC,CAAC;YACF,OAAO,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;QAClC,CAAC,CAAC,CAAC;IACL,CAAC;IAED,4FAA4F;IAC5F,gBAAgB,CAAC,IAAY;QAC3B,OAAO,IAAI,oBAAoB,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACrD,CAAC;CACF;AAED,wGAAwG;AACxG,MAAM,OAAO,oBAAoB;IAOF;IAA2C;IAN/D,QAAQ,GAAG,MAAM,CAAwB,IAAI,CAAC,CAAC;IAC/C,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAC3B,KAAK,GAAG,MAAM,CAAyB,IAAI,CAAC,CAAC;IAC7C,MAAM,GAAG,MAAM,CAAsB,IAAI,CAAC,CAAC;IAC5C,UAAU,GAA2B,IAAI,CAAC;IAElD,YAA6B,MAAwB,EAAmB,IAAY;QAAvD,WAAM,GAAN,MAAM,CAAkB;QAAmB,SAAI,GAAJ,IAAI,CAAQ;IAAG,CAAC;IAExF,KAAK,CAAC,MAAM,CAAC,KAAiD;QAC5D,IAAI,IAAI,CAAC,UAAU,EAAE;YAAE,OAAO,IAAI,CAAC;QACnC,IAAI,CAAC,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACxC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC1B,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACrB,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACtB,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,GAAG,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,UAAU,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;YACxJ,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YACxB,OAAO,MAAM,CAAC;QAChB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,YAAY,eAAe,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,eAAe,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,kCAAkC,EAAE,QAAQ,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;YACxL,OAAO,IAAI,CAAC;QACd,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YAC3B,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;QACzB,CAAC;IACH,CAAC;IAED,MAAM;QACJ,IAAI,CAAC,UAAU,EAAE,KAAK,EAAE,CAAC;IAC3B,CAAC;CACF;AAED,wGAAwG;AACxG,MAAM,UAAU,iBAAiB,CAAC,MAAwB;IACxD,OAAO;QACL,EAAE,OAAO,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,EAAE;QACjD,EAAE,OAAO,EAAE,iBAAiB,EAAE,UAAU,EAAE,CAAC,KAAuB,EAAE,EAAE,CAAC,IAAI,iBAAiB,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC,kBAAkB,CAAC,EAAE;KAClI,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"captcha.d.ts","sourceRoot":"","sources":["../src/captcha.ts"],"names":[],"mappings":"AA4BA,+HAA+H;AAC/H,wBAAsB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAMrE"}
|
package/dist/captcha.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { PublicFormError } from './errors.js';
|
|
2
|
+
/** The action LayrStack's own published form uses; the server only checks the score. */
|
|
3
|
+
const ACTION = 'public_submit';
|
|
4
|
+
function loadScript(siteKey) {
|
|
5
|
+
const host = window;
|
|
6
|
+
if (host.grecaptcha)
|
|
7
|
+
return Promise.resolve();
|
|
8
|
+
const fail = () => new PublicFormError('CAPTCHA could not be loaded.', 'captcha', { retryable: true });
|
|
9
|
+
const existing = document.querySelector('script[data-layrstack-recaptcha]');
|
|
10
|
+
if (existing)
|
|
11
|
+
return new Promise((resolve, reject) => { existing.addEventListener('load', () => resolve(), { once: true }); existing.addEventListener('error', () => reject(fail()), { once: true }); });
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
const script = document.createElement('script');
|
|
14
|
+
script.async = true;
|
|
15
|
+
script.defer = true;
|
|
16
|
+
script.src = `https://www.google.com/recaptcha/api.js?render=${encodeURIComponent(siteKey)}`;
|
|
17
|
+
script.dataset['layrstackRecaptcha'] = 'true';
|
|
18
|
+
script.onload = () => resolve();
|
|
19
|
+
script.onerror = () => reject(fail());
|
|
20
|
+
document.head.appendChild(script);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/** A fresh reCAPTCHA v3 token. Tokens are single-use and expire in about two minutes, so call this right before the submit. */
|
|
24
|
+
export async function recaptchaToken(siteKey) {
|
|
25
|
+
if (typeof window === 'undefined')
|
|
26
|
+
throw new PublicFormError('CAPTCHA needs a browser. Pass getCaptchaToken in the config.', 'captcha');
|
|
27
|
+
await loadScript(siteKey);
|
|
28
|
+
const grecaptcha = window.grecaptcha;
|
|
29
|
+
if (!grecaptcha)
|
|
30
|
+
throw new PublicFormError('CAPTCHA could not be loaded.', 'captcha', { retryable: true });
|
|
31
|
+
return new Promise((resolve, reject) => grecaptcha.ready(() => grecaptcha.execute(siteKey, { action: ACTION }).then(resolve, (cause) => reject(new PublicFormError('CAPTCHA verification failed to start.', 'captcha', { retryable: true, cause })))));
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=captcha.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"captcha.js","sourceRoot":"","sources":["../src/captcha.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAQ9C,wFAAwF;AACxF,MAAM,MAAM,GAAG,eAAe,CAAC;AAE/B,SAAS,UAAU,CAAC,OAAe;IACjC,MAAM,IAAI,GAAG,MAAuB,CAAC;IACrC,IAAI,IAAI,CAAC,UAAU;QAAE,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;IAC9C,MAAM,IAAI,GAAG,GAAG,EAAE,CAAC,IAAI,eAAe,CAAC,8BAA8B,EAAE,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACvG,MAAM,QAAQ,GAAG,QAAQ,CAAC,aAAa,CAAoB,kCAAkC,CAAC,CAAC;IAC/F,IAAI,QAAQ;QAAE,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,GAAG,QAAQ,CAAC,gBAAgB,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACzM,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,MAAM,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;QAChD,MAAM,CAAC,KAAK,GAAG,IAAI,CAAC;QAAC,MAAM,CAAC,KAAK,GAAG,IAAI,CAAC;QACzC,MAAM,CAAC,GAAG,GAAG,kDAAkD,kBAAkB,CAAC,OAAO,CAAC,EAAE,CAAC;QAC7F,MAAM,CAAC,OAAO,CAAC,oBAAoB,CAAC,GAAG,MAAM,CAAC;QAC9C,MAAM,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC;QAChC,MAAM,CAAC,OAAO,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;QACtC,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;AACL,CAAC;AAED,+HAA+H;AAC/H,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,OAAe;IAClD,IAAI,OAAO,MAAM,KAAK,WAAW;QAAE,MAAM,IAAI,eAAe,CAAC,8DAA8D,EAAE,SAAS,CAAC,CAAC;IACxI,MAAM,UAAU,CAAC,OAAO,CAAC,CAAC;IAC1B,MAAM,UAAU,GAAI,MAAwB,CAAC,UAAU,CAAC;IACxD,IAAI,CAAC,UAAU;QAAE,MAAM,IAAI,eAAe,CAAC,8BAA8B,EAAE,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3G,OAAO,IAAI,OAAO,CAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,eAAe,CAAC,uCAAuC,EAAE,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACjQ,CAAC"}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { PublicFormConfig, PublicRecord, PublicViewContract, SubmitInput } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* One call for a public form with files: validates the files against the form's rules, uploads each one (upload intent, direct PUT
|
|
4
|
+
* to storage, complete), gets a fresh reCAPTCHA token when the form needs one, then submits the record with the file ids.
|
|
5
|
+
*
|
|
6
|
+
* A failed `submit` can simply be called again with the same File objects: files that already uploaded are not sent twice.
|
|
7
|
+
*/
|
|
8
|
+
export declare class PublicFormClient {
|
|
9
|
+
private readonly config;
|
|
10
|
+
private readonly contracts;
|
|
11
|
+
/** Uploaded file ids by view and field, keyed by the File object so a retry reuses them. */
|
|
12
|
+
private readonly uploaded;
|
|
13
|
+
constructor(config: PublicFormConfig);
|
|
14
|
+
/** The form's create fields, options and policy (cached per view). Useful to render the form as well. */
|
|
15
|
+
getContract(view: string, options?: {
|
|
16
|
+
refresh?: boolean;
|
|
17
|
+
signal?: AbortSignal;
|
|
18
|
+
}): Promise<PublicViewContract>;
|
|
19
|
+
submit(view: string, input: SubmitInput): Promise<PublicRecord>;
|
|
20
|
+
private uploadFile;
|
|
21
|
+
private captcha;
|
|
22
|
+
/** JSON call to the public API; failures become PublicFormError with the server's message. Never retried here. */
|
|
23
|
+
private request;
|
|
24
|
+
private viewUrl;
|
|
25
|
+
private cacheFor;
|
|
26
|
+
}
|
|
27
|
+
export declare function createPublicFormClient(config: PublicFormConfig): PublicFormClient;
|
|
28
|
+
/** One-off submit without keeping a client (no retry cache between calls). */
|
|
29
|
+
export declare function submitPublicForm(config: PublicFormConfig & SubmitInput & {
|
|
30
|
+
view: string;
|
|
31
|
+
}): Promise<PublicRecord>;
|
|
32
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAgB,gBAAgB,EAAE,YAAY,EAAE,kBAAkB,EAAE,WAAW,EAA+B,MAAM,YAAY,CAAC;AAQ7I;;;;;GAKG;AACH,qBAAa,gBAAgB;IAKf,OAAO,CAAC,QAAQ,CAAC,MAAM;IAJnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAkD;IAC5E,4FAA4F;IAC5F,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA4C;gBAExC,MAAM,EAAE,gBAAgB;IAErD,yGAAyG;IACzG,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAO,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAW3G,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;YAwDvD,UAAU;YA4BV,OAAO;IAOrB,kHAAkH;YACpG,OAAO;IAkBrB,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,QAAQ;CAMjB;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,gBAAgB,GAAG,gBAAgB,CAEjF;AAED,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,WAAW,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,YAAY,CAAC,CAGjH"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { recaptchaToken } from './captcha.js';
|
|
2
|
+
import { PublicFormError, serverMessage } from './errors.js';
|
|
3
|
+
import { StorageError, putFileToStorage } from './transport.js';
|
|
4
|
+
import { contentTypeOf, validateFiles } from './validate.js';
|
|
5
|
+
/** Share of the overall progress taken by the file uploads; the rest is the final submit. */
|
|
6
|
+
const UPLOAD_SHARE = 90;
|
|
7
|
+
/**
|
|
8
|
+
* One call for a public form with files: validates the files against the form's rules, uploads each one (upload intent, direct PUT
|
|
9
|
+
* to storage, complete), gets a fresh reCAPTCHA token when the form needs one, then submits the record with the file ids.
|
|
10
|
+
*
|
|
11
|
+
* A failed `submit` can simply be called again with the same File objects: files that already uploaded are not sent twice.
|
|
12
|
+
*/
|
|
13
|
+
export class PublicFormClient {
|
|
14
|
+
config;
|
|
15
|
+
contracts = new Map();
|
|
16
|
+
/** Uploaded file ids by view and field, keyed by the File object so a retry reuses them. */
|
|
17
|
+
uploaded = new Map();
|
|
18
|
+
constructor(config) {
|
|
19
|
+
this.config = config;
|
|
20
|
+
}
|
|
21
|
+
/** The form's create fields, options and policy (cached per view). Useful to render the form as well. */
|
|
22
|
+
getContract(view, options = {}) {
|
|
23
|
+
if (options.refresh)
|
|
24
|
+
this.contracts.delete(view);
|
|
25
|
+
let contract = this.contracts.get(view);
|
|
26
|
+
if (!contract) {
|
|
27
|
+
contract = this.request('GET', this.viewUrl(view), undefined, 'contract', {}, options.signal);
|
|
28
|
+
this.contracts.set(view, contract);
|
|
29
|
+
contract.catch(() => this.contracts.delete(view));
|
|
30
|
+
}
|
|
31
|
+
return contract;
|
|
32
|
+
}
|
|
33
|
+
async submit(view, input) {
|
|
34
|
+
const { signal } = input;
|
|
35
|
+
const progress = { stage: 'contract', percent: 0, files: [] };
|
|
36
|
+
const emit = (stage, percent) => {
|
|
37
|
+
if (stage)
|
|
38
|
+
progress.stage = stage;
|
|
39
|
+
if (percent !== undefined)
|
|
40
|
+
progress.percent = percent;
|
|
41
|
+
input.onProgress?.({ ...progress, files: progress.files.map((file) => ({ ...file })) });
|
|
42
|
+
};
|
|
43
|
+
emit();
|
|
44
|
+
const contract = await this.getContract(view, { signal });
|
|
45
|
+
if (!contract.public.api.create.enabled)
|
|
46
|
+
throw new PublicFormError('This form does not accept submissions.', 'contract');
|
|
47
|
+
emit('validate', 0);
|
|
48
|
+
const files = normalizeFiles(input.files);
|
|
49
|
+
validateFiles(contract, files);
|
|
50
|
+
const jobs = [];
|
|
51
|
+
for (const [field, list] of Object.entries(files)) {
|
|
52
|
+
const cache = this.cacheFor(view, field);
|
|
53
|
+
for (const file of list) {
|
|
54
|
+
const fileId = cache.get(file);
|
|
55
|
+
const entry = { field, name: file.name, size: file.size, status: fileId ? 'done' : 'queued', percent: fileId ? 100 : 0, fileId };
|
|
56
|
+
progress.files.push(entry);
|
|
57
|
+
if (!fileId)
|
|
58
|
+
jobs.push({ field, file, entry });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
emit('upload', 0);
|
|
62
|
+
const totalBytes = progress.files.reduce((sum, file) => sum + file.size, 0);
|
|
63
|
+
const overall = () => (totalBytes ? progress.files.reduce((sum, file) => sum + file.size * file.percent / 100, 0) / totalBytes * UPLOAD_SHARE : UPLOAD_SHARE);
|
|
64
|
+
emit('upload', overall());
|
|
65
|
+
await runPool(jobs, Math.max(1, this.config.concurrency ?? 3), async (job) => {
|
|
66
|
+
job.entry.fileId = await this.uploadFile(view, job.field, job.file, (patch) => { Object.assign(job.entry, patch); emit(undefined, overall()); }, signal);
|
|
67
|
+
});
|
|
68
|
+
const value = { ...input.values };
|
|
69
|
+
for (const field of Object.keys(files)) {
|
|
70
|
+
const ids = progress.files.filter((file) => file.field === field).map((file) => file.fileId);
|
|
71
|
+
if (ids.length)
|
|
72
|
+
value[field] = ids;
|
|
73
|
+
}
|
|
74
|
+
const headers = {};
|
|
75
|
+
if (contract.public.api.create.captchaRequired) {
|
|
76
|
+
emit('captcha', UPLOAD_SHARE);
|
|
77
|
+
// Last step before the submit: the token is single-use and short-lived, and uploads can take longer than it lives.
|
|
78
|
+
headers['X-Captcha-Token'] = await this.captcha(contract, signal);
|
|
79
|
+
}
|
|
80
|
+
emit('submit', 95);
|
|
81
|
+
const record = await this.request('POST', `${this.viewUrl(view)}/submit`, { value }, 'submit', {}, signal, headers);
|
|
82
|
+
for (const field of Object.keys(files))
|
|
83
|
+
for (const file of files[field] ?? [])
|
|
84
|
+
this.cacheFor(view, field).delete(file);
|
|
85
|
+
emit('done', 100);
|
|
86
|
+
return record;
|
|
87
|
+
}
|
|
88
|
+
async uploadFile(view, field, file, update, signal) {
|
|
89
|
+
const meta = { field, fileName: file.name };
|
|
90
|
+
const contentType = contentTypeOf(file);
|
|
91
|
+
const retries = this.config.retries ?? 2;
|
|
92
|
+
for (let attempt = 0;; attempt++) {
|
|
93
|
+
try {
|
|
94
|
+
update({ status: 'uploading', percent: 0 });
|
|
95
|
+
// A new intent per attempt: a failed or expired URL is simply replaced.
|
|
96
|
+
const intent = await this.request('POST', `${this.viewUrl(view)}/files/upload-intent`, { field, originalName: file.name, contentType, sizeBytes: file.size }, 'upload', meta, signal);
|
|
97
|
+
await (this.config.putFile ?? putFileToStorage)({ url: intent.uploadUrl, file, contentType, signal, onProgress: (fraction) => update({ percent: Math.min(99, fraction * 100) }) });
|
|
98
|
+
update({ status: 'completing', percent: 99 });
|
|
99
|
+
await this.request('POST', `${this.viewUrl(view)}/files/${encodeURIComponent(intent.file.id)}/complete`, {}, 'upload', meta, signal);
|
|
100
|
+
this.cacheFor(view, field).set(file, intent.file.id);
|
|
101
|
+
update({ status: 'done', percent: 100, fileId: intent.file.id });
|
|
102
|
+
return intent.file.id;
|
|
103
|
+
}
|
|
104
|
+
catch (cause) {
|
|
105
|
+
const error = cause instanceof StorageError
|
|
106
|
+
? new PublicFormError(`${file.name}: ${cause.message}`, 'upload', { ...meta, status: cause.status, retryable: true, cause })
|
|
107
|
+
: cause instanceof PublicFormError ? cause : new PublicFormError(`${file.name}: upload failed.`, 'upload', { ...meta, cause });
|
|
108
|
+
if (error.stage === 'aborted' || !error.retryable || attempt >= retries) {
|
|
109
|
+
update({ status: 'failed' });
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
await delay((this.config.retryDelayMs ?? 500) * 2 ** attempt, signal);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async captcha(contract, signal) {
|
|
117
|
+
const siteKey = this.config.recaptchaSiteKey ?? contract.public.recaptchaSiteKey;
|
|
118
|
+
if (!siteKey)
|
|
119
|
+
throw new PublicFormError('CAPTCHA is required but this form has no reCAPTCHA site key.', 'captcha');
|
|
120
|
+
if (signal?.aborted)
|
|
121
|
+
throw new PublicFormError('The submission was cancelled.', 'aborted');
|
|
122
|
+
return (this.config.getCaptchaToken ?? recaptchaToken)(siteKey);
|
|
123
|
+
}
|
|
124
|
+
/** JSON call to the public API; failures become PublicFormError with the server's message. Never retried here. */
|
|
125
|
+
async request(method, url, body, stage, meta, signal, extra = {}) {
|
|
126
|
+
const headers = { ...extra };
|
|
127
|
+
if (this.config.token)
|
|
128
|
+
headers['X-Public-Token'] = this.config.token;
|
|
129
|
+
if (body !== undefined)
|
|
130
|
+
headers['Content-Type'] = 'application/json';
|
|
131
|
+
let response;
|
|
132
|
+
try {
|
|
133
|
+
response = await (this.config.fetch ?? fetch)(url, { method, headers, body: body === undefined ? undefined : JSON.stringify(body), signal });
|
|
134
|
+
}
|
|
135
|
+
catch (cause) {
|
|
136
|
+
if (signal?.aborted || cause?.name === 'AbortError')
|
|
137
|
+
throw new PublicFormError('The submission was cancelled.', 'aborted', { ...meta, cause });
|
|
138
|
+
throw new PublicFormError('The server could not be reached. Check the connection and try again.', stage, { ...meta, retryable: true, ambiguous: stage === 'submit', cause });
|
|
139
|
+
}
|
|
140
|
+
if (!response.ok) {
|
|
141
|
+
const message = await serverMessage(response, stage === 'submit' ? 'The form could not be submitted' : stage === 'upload' ? 'The upload was refused' : 'The form could not be loaded');
|
|
142
|
+
throw new PublicFormError(message, stage, { ...meta, status: response.status, retryable: response.status === 429 || response.status >= 500 });
|
|
143
|
+
}
|
|
144
|
+
return await response.json();
|
|
145
|
+
}
|
|
146
|
+
viewUrl(view) {
|
|
147
|
+
return `${this.config.apiBase.replace(/\/+$/, '')}/public/${encodeURIComponent(this.config.tenant)}/views/${encodeURIComponent(view)}`;
|
|
148
|
+
}
|
|
149
|
+
cacheFor(view, field) {
|
|
150
|
+
const key = `${view}|${field}`;
|
|
151
|
+
let cache = this.uploaded.get(key);
|
|
152
|
+
if (!cache)
|
|
153
|
+
this.uploaded.set(key, cache = new WeakMap());
|
|
154
|
+
return cache;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
export function createPublicFormClient(config) {
|
|
158
|
+
return new PublicFormClient(config);
|
|
159
|
+
}
|
|
160
|
+
/** One-off submit without keeping a client (no retry cache between calls). */
|
|
161
|
+
export function submitPublicForm(config) {
|
|
162
|
+
const { view, values, files, onProgress, signal, ...rest } = config;
|
|
163
|
+
return new PublicFormClient(rest).submit(view, { values, files, onProgress, signal });
|
|
164
|
+
}
|
|
165
|
+
function normalizeFiles(files) {
|
|
166
|
+
const output = {};
|
|
167
|
+
for (const [field, value] of Object.entries(files ?? {})) {
|
|
168
|
+
const list = Array.isArray(value) ? value : value ? [value] : [];
|
|
169
|
+
if (list.length)
|
|
170
|
+
output[field] = list;
|
|
171
|
+
}
|
|
172
|
+
return output;
|
|
173
|
+
}
|
|
174
|
+
/** Runs `work` over `items`, `limit` at a time. After the first failure nothing new starts; work in flight finishes (so its files stay cached for a retry). */
|
|
175
|
+
async function runPool(items, limit, work) {
|
|
176
|
+
let next = 0;
|
|
177
|
+
let failure;
|
|
178
|
+
const worker = async () => {
|
|
179
|
+
while (!failure) {
|
|
180
|
+
const index = next++;
|
|
181
|
+
if (index >= items.length)
|
|
182
|
+
return;
|
|
183
|
+
try {
|
|
184
|
+
await work(items[index]);
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
failure ??= { error };
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
192
|
+
if (failure)
|
|
193
|
+
throw failure.error;
|
|
194
|
+
}
|
|
195
|
+
function delay(ms, signal) {
|
|
196
|
+
return new Promise((resolve, reject) => {
|
|
197
|
+
const aborted = () => reject(new PublicFormError('The submission was cancelled.', 'aborted'));
|
|
198
|
+
if (signal?.aborted)
|
|
199
|
+
return aborted();
|
|
200
|
+
const timer = setTimeout(() => { signal?.removeEventListener('abort', onAbort); resolve(); }, ms);
|
|
201
|
+
const onAbort = () => { clearTimeout(timer); aborted(); };
|
|
202
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { SubmitStage } from './types.js';
|
|
2
|
+
/** Every failure of `submit`, with where it happened so a form can show it next to the right field or file. */
|
|
3
|
+
export declare class PublicFormError extends Error {
|
|
4
|
+
readonly stage: Exclude<SubmitStage, 'done'> | 'aborted';
|
|
5
|
+
readonly options: {
|
|
6
|
+
field?: string;
|
|
7
|
+
fileName?: string;
|
|
8
|
+
/** HTTP status of the failed call, when there was one. */
|
|
9
|
+
status?: number;
|
|
10
|
+
/** True when calling `submit` again is likely to work (already uploaded files are not sent again). */
|
|
11
|
+
retryable?: boolean;
|
|
12
|
+
/** The final submit may have reached the server; check before sending it again. */
|
|
13
|
+
ambiguous?: boolean;
|
|
14
|
+
cause?: unknown;
|
|
15
|
+
};
|
|
16
|
+
readonly name = "PublicFormError";
|
|
17
|
+
constructor(message: string, stage: Exclude<SubmitStage, 'done'> | 'aborted', options?: {
|
|
18
|
+
field?: string;
|
|
19
|
+
fileName?: string;
|
|
20
|
+
/** HTTP status of the failed call, when there was one. */
|
|
21
|
+
status?: number;
|
|
22
|
+
/** True when calling `submit` again is likely to work (already uploaded files are not sent again). */
|
|
23
|
+
retryable?: boolean;
|
|
24
|
+
/** The final submit may have reached the server; check before sending it again. */
|
|
25
|
+
ambiguous?: boolean;
|
|
26
|
+
cause?: unknown;
|
|
27
|
+
});
|
|
28
|
+
get field(): string | undefined;
|
|
29
|
+
get fileName(): string | undefined;
|
|
30
|
+
get status(): number | undefined;
|
|
31
|
+
get retryable(): boolean;
|
|
32
|
+
get ambiguous(): boolean;
|
|
33
|
+
}
|
|
34
|
+
/** The server's `message` (a string, or the first of a list), else a generic one for the status. */
|
|
35
|
+
export declare function serverMessage(response: Response, fallback: string): Promise<string>;
|
|
36
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,+GAA+G;AAC/G,qBAAa,eAAgB,SAAQ,KAAK;IAKtC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,WAAW,EAAE,MAAM,CAAC,GAAG,SAAS;IACxD,QAAQ,CAAC,OAAO,EAAE;QAChB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,0DAA0D;QAC1D,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,sGAAsG;QACtG,SAAS,CAAC,EAAE,OAAO,CAAC;QACpB,mFAAmF;QACnF,SAAS,CAAC,EAAE,OAAO,CAAC;QACpB,KAAK,CAAC,EAAE,OAAO,CAAC;KACjB;IAfH,SAAkB,IAAI,qBAAqB;gBAGzC,OAAO,EAAE,MAAM,EACN,KAAK,EAAE,OAAO,CAAC,WAAW,EAAE,MAAM,CAAC,GAAG,SAAS,EAC/C,OAAO,GAAE;QAChB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,0DAA0D;QAC1D,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,sGAAsG;QACtG,SAAS,CAAC,EAAE,OAAO,CAAC;QACpB,mFAAmF;QACnF,SAAS,CAAC,EAAE,OAAO,CAAC;QACpB,KAAK,CAAC,EAAE,OAAO,CAAC;KACZ;IAIR,IAAI,KAAK,IAAI,MAAM,GAAG,SAAS,CAA+B;IAC9D,IAAI,QAAQ,IAAI,MAAM,GAAG,SAAS,CAAkC;IACpE,IAAI,MAAM,IAAI,MAAM,GAAG,SAAS,CAAgC;IAChE,IAAI,SAAS,IAAI,OAAO,CAA4C;IACpE,IAAI,SAAS,IAAI,OAAO,CAA4C;CACrE;AAED,oGAAoG;AACpG,wBAAsB,aAAa,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAOzF"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Every failure of `submit`, with where it happened so a form can show it next to the right field or file. */
|
|
2
|
+
export class PublicFormError extends Error {
|
|
3
|
+
stage;
|
|
4
|
+
options;
|
|
5
|
+
name = 'PublicFormError';
|
|
6
|
+
constructor(message, stage, options = {}) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.stage = stage;
|
|
9
|
+
this.options = options;
|
|
10
|
+
}
|
|
11
|
+
get field() { return this.options.field; }
|
|
12
|
+
get fileName() { return this.options.fileName; }
|
|
13
|
+
get status() { return this.options.status; }
|
|
14
|
+
get retryable() { return this.options.retryable ?? false; }
|
|
15
|
+
get ambiguous() { return this.options.ambiguous ?? false; }
|
|
16
|
+
}
|
|
17
|
+
/** The server's `message` (a string, or the first of a list), else a generic one for the status. */
|
|
18
|
+
export async function serverMessage(response, fallback) {
|
|
19
|
+
try {
|
|
20
|
+
const body = await response.json();
|
|
21
|
+
if (typeof body.message === 'string' && body.message)
|
|
22
|
+
return body.message;
|
|
23
|
+
if (Array.isArray(body.message) && typeof body.message[0] === 'string')
|
|
24
|
+
return body.message[0];
|
|
25
|
+
}
|
|
26
|
+
catch { /* not JSON */ }
|
|
27
|
+
return `${fallback} (HTTP ${response.status}).`;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAEA,+GAA+G;AAC/G,MAAM,OAAO,eAAgB,SAAQ,KAAK;IAK7B;IACA;IALO,IAAI,GAAG,iBAAiB,CAAC;IAE3C,YACE,OAAe,EACN,KAA+C,EAC/C,UAUL,EAAE;QAEN,KAAK,CAAC,OAAO,CAAC,CAAC;QAbN,UAAK,GAAL,KAAK,CAA0C;QAC/C,YAAO,GAAP,OAAO,CAUV;IAGR,CAAC;IACD,IAAI,KAAK,KAAyB,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;IAC9D,IAAI,QAAQ,KAAyB,OAAO,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;IACpE,IAAI,MAAM,KAAyB,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;IAChE,IAAI,SAAS,KAAc,OAAO,IAAI,CAAC,OAAO,CAAC,SAAS,IAAI,KAAK,CAAC,CAAC,CAAC;IACpE,IAAI,SAAS,KAAc,OAAO,IAAI,CAAC,OAAO,CAAC,SAAS,IAAI,KAAK,CAAC,CAAC,CAAC;CACrE;AAED,oGAAoG;AACpG,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,QAAkB,EAAE,QAAgB;IACtE,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAA2B,CAAC;QAC5D,IAAI,OAAO,IAAI,CAAC,OAAO,KAAK,QAAQ,IAAI,IAAI,CAAC,OAAO;YAAE,OAAO,IAAI,CAAC,OAAO,CAAC;QAC1E,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,OAAO,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IACjG,CAAC;IAAC,MAAM,CAAC,CAAC,cAAc,CAAC,CAAC;IAC1B,OAAO,GAAG,QAAQ,UAAU,QAAQ,CAAC,MAAM,IAAI,CAAC;AAClD,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { PublicFormClient, createPublicFormClient, submitPublicForm } from './client.js';
|
|
2
|
+
export { PublicFormError } from './errors.js';
|
|
3
|
+
export { uploadFieldTypes, isUploadField, validateFiles } from './validate.js';
|
|
4
|
+
export { putFileToStorage } from './transport.js';
|
|
5
|
+
export { recaptchaToken } from './captcha.js';
|
|
6
|
+
export type { FileProgress, FileStatus, PublicContractField, PublicFormConfig, PublicRecord, PublicViewContract, PutFile, SubmitInput, SubmitProgress, SubmitStage, } 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,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACzF,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC/E,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,YAAY,EACV,YAAY,EAAE,UAAU,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,YAAY,EAAE,kBAAkB,EAAE,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,WAAW,GACrJ,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { PublicFormClient, createPublicFormClient, submitPublicForm } from './client.js';
|
|
2
|
+
export { PublicFormError } from './errors.js';
|
|
3
|
+
export { uploadFieldTypes, isUploadField, validateFiles } from './validate.js';
|
|
4
|
+
export { putFileToStorage } from './transport.js';
|
|
5
|
+
export { recaptchaToken } from './captcha.js';
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACzF,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC/E,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PutFile } from './types.js';
|
|
2
|
+
/** Raised for a storage PUT that is worth repeating (network error, 5xx, expired signature). */
|
|
3
|
+
export declare class StorageError extends Error {
|
|
4
|
+
readonly status?: number | undefined;
|
|
5
|
+
constructor(message: string, status?: number | undefined);
|
|
6
|
+
}
|
|
7
|
+
/** Direct upload to the presigned URL. XMLHttpRequest reports progress; where it is missing (SSR, tests) `fetch` is used. */
|
|
8
|
+
export declare const putFileToStorage: PutFile;
|
|
9
|
+
//# sourceMappingURL=transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE1C,gGAAgG;AAChG,qBAAa,YAAa,SAAQ,KAAK;IACR,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM;gBAAzC,OAAO,EAAE,MAAM,EAAW,MAAM,CAAC,EAAE,MAAM,YAAA;CACtD;AAED,6HAA6H;AAC7H,eAAO,MAAM,gBAAgB,EAAE,OAwB9B,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { PublicFormError } from './errors.js';
|
|
2
|
+
/** Raised for a storage PUT that is worth repeating (network error, 5xx, expired signature). */
|
|
3
|
+
export class StorageError extends Error {
|
|
4
|
+
status;
|
|
5
|
+
constructor(message, status) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.status = status;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/** Direct upload to the presigned URL. XMLHttpRequest reports progress; where it is missing (SSR, tests) `fetch` is used. */
|
|
11
|
+
export const putFileToStorage = ({ url, file, contentType, onProgress, signal }) => {
|
|
12
|
+
if (typeof XMLHttpRequest === 'undefined') {
|
|
13
|
+
return fetch(url, { method: 'PUT', headers: { 'Content-Type': contentType }, body: file, signal }).then((response) => {
|
|
14
|
+
if (!response.ok)
|
|
15
|
+
throw new StorageError(`The file could not be stored (HTTP ${response.status}).`, response.status);
|
|
16
|
+
onProgress(1);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const request = new XMLHttpRequest();
|
|
21
|
+
const aborted = () => reject(new PublicFormError('The upload was cancelled.', 'aborted'));
|
|
22
|
+
if (signal?.aborted)
|
|
23
|
+
return aborted();
|
|
24
|
+
request.open('PUT', url);
|
|
25
|
+
// The URL is signed for this exact Content-Type; sending anything else fails with SignatureDoesNotMatch.
|
|
26
|
+
request.setRequestHeader('Content-Type', contentType);
|
|
27
|
+
request.upload.onprogress = (event) => { if (event.lengthComputable && event.total > 0)
|
|
28
|
+
onProgress(event.loaded / event.total); };
|
|
29
|
+
request.onload = () => {
|
|
30
|
+
if (request.status >= 200 && request.status < 300) {
|
|
31
|
+
onProgress(1);
|
|
32
|
+
resolve();
|
|
33
|
+
}
|
|
34
|
+
else
|
|
35
|
+
reject(new StorageError(`The file could not be stored (HTTP ${request.status}).`, request.status));
|
|
36
|
+
};
|
|
37
|
+
request.onerror = () => reject(new StorageError('The file could not be sent. Check the connection, and that the storage bucket allows this site (CORS).'));
|
|
38
|
+
request.onabort = aborted;
|
|
39
|
+
signal?.addEventListener('abort', () => request.abort(), { once: true });
|
|
40
|
+
request.send(file);
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
//# sourceMappingURL=transport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAG9C,gGAAgG;AAChG,MAAM,OAAO,YAAa,SAAQ,KAAK;IACC;IAAtC,YAAY,OAAe,EAAW,MAAe;QAAI,KAAK,CAAC,OAAO,CAAC,CAAC;QAAlC,WAAM,GAAN,MAAM,CAAS;IAAoB,CAAC;CAC3E;AAED,6HAA6H;AAC7H,MAAM,CAAC,MAAM,gBAAgB,GAAY,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE;IAC1F,IAAI,OAAO,cAAc,KAAK,WAAW,EAAE,CAAC;QAC1C,OAAO,KAAK,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,cAAc,EAAE,WAAW,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE;YACnH,IAAI,CAAC,QAAQ,CAAC,EAAE;gBAAE,MAAM,IAAI,YAAY,CAAC,sCAAsC,QAAQ,CAAC,MAAM,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;YACrH,UAAU,CAAC,CAAC,CAAC,CAAC;QAChB,CAAC,CAAC,CAAC;IACL,CAAC;IACD,OAAO,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC3C,MAAM,OAAO,GAAG,IAAI,cAAc,EAAE,CAAC;QACrC,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,eAAe,CAAC,2BAA2B,EAAE,SAAS,CAAC,CAAC,CAAC;QAC1F,IAAI,MAAM,EAAE,OAAO;YAAE,OAAO,OAAO,EAAE,CAAC;QACtC,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QACzB,yGAAyG;QACzG,OAAO,CAAC,gBAAgB,CAAC,cAAc,EAAE,WAAW,CAAC,CAAC;QACtD,OAAO,CAAC,MAAM,CAAC,UAAU,GAAG,CAAC,KAAK,EAAE,EAAE,GAAG,IAAI,KAAK,CAAC,gBAAgB,IAAI,KAAK,CAAC,KAAK,GAAG,CAAC;YAAE,UAAU,CAAC,KAAK,CAAC,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QAClI,OAAO,CAAC,MAAM,GAAG,GAAG,EAAE;YACpB,IAAI,OAAO,CAAC,MAAM,IAAI,GAAG,IAAI,OAAO,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;gBAAC,UAAU,CAAC,CAAC,CAAC,CAAC;gBAAC,OAAO,EAAE,CAAC;YAAC,CAAC;;gBAC3E,MAAM,CAAC,IAAI,YAAY,CAAC,sCAAsC,OAAO,CAAC,MAAM,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;QAC1G,CAAC,CAAC;QACF,OAAO,CAAC,OAAO,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,YAAY,CAAC,wGAAwG,CAAC,CAAC,CAAC;QAC3J,OAAO,CAAC,OAAO,GAAG,OAAO,CAAC;QAC1B,MAAM,EAAE,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QACzE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACrB,CAAC,CAAC,CAAC;AACL,CAAC,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/** A function that PUTs the bytes to a presigned storage URL and reports the share sent (0 to 1). */
|
|
2
|
+
export type PutFile = (request: {
|
|
3
|
+
url: string;
|
|
4
|
+
file: File;
|
|
5
|
+
contentType: string;
|
|
6
|
+
onProgress: (fraction: number) => void;
|
|
7
|
+
signal?: AbortSignal;
|
|
8
|
+
}) => Promise<void>;
|
|
9
|
+
export interface PublicFormConfig {
|
|
10
|
+
/** API base including `/api` on the tenant host, e.g. `https://acme.api.example.com/api`. */
|
|
11
|
+
apiBase: string;
|
|
12
|
+
/** Tenant slug, as in `/public/:tenant/views/:view`. */
|
|
13
|
+
tenant: string;
|
|
14
|
+
/** Public access token (token-mode views only). Never ship a token in a public website. */
|
|
15
|
+
token?: string;
|
|
16
|
+
/** Files uploaded at the same time. Default 3. */
|
|
17
|
+
concurrency?: number;
|
|
18
|
+
/** Extra attempts for an upload that fails on a network, storage or 5xx error. Default 2. */
|
|
19
|
+
retries?: number;
|
|
20
|
+
/** Wait before the first upload retry, doubled each time. Default 500 ms. */
|
|
21
|
+
retryDelayMs?: number;
|
|
22
|
+
/** Overrides the reCAPTCHA site key from the view contract. */
|
|
23
|
+
recaptchaSiteKey?: string;
|
|
24
|
+
/** Supplies the reCAPTCHA token yourself instead of loading Google's script. Called right before the submit. */
|
|
25
|
+
getCaptchaToken?: (siteKey: string) => Promise<string>;
|
|
26
|
+
/** Custom `fetch` (tests, SSR). Defaults to the global one. */
|
|
27
|
+
fetch?: typeof fetch;
|
|
28
|
+
/** Custom storage upload (tests, SSR). Defaults to XMLHttpRequest, which reports progress. */
|
|
29
|
+
putFile?: PutFile;
|
|
30
|
+
}
|
|
31
|
+
/** The create fields and policy of a public view (GET /public/:tenant/views/:view). */
|
|
32
|
+
export interface PublicViewContract {
|
|
33
|
+
id: string;
|
|
34
|
+
slug: string;
|
|
35
|
+
name: string;
|
|
36
|
+
description?: string;
|
|
37
|
+
fields: PublicContractField[];
|
|
38
|
+
fieldOptions: Record<string, Array<{
|
|
39
|
+
label: string;
|
|
40
|
+
value: string;
|
|
41
|
+
}>>;
|
|
42
|
+
public: {
|
|
43
|
+
mode: 'public' | 'token';
|
|
44
|
+
api: {
|
|
45
|
+
create: {
|
|
46
|
+
enabled: boolean;
|
|
47
|
+
fieldKeys: string[];
|
|
48
|
+
captchaRequired: boolean;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
recaptchaSiteKey?: string | null;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export interface PublicContractField {
|
|
55
|
+
id: string;
|
|
56
|
+
name: string;
|
|
57
|
+
label: string;
|
|
58
|
+
type: string;
|
|
59
|
+
required?: boolean;
|
|
60
|
+
file?: {
|
|
61
|
+
maxFiles?: number;
|
|
62
|
+
maxSizeBytes?: number;
|
|
63
|
+
accept?: string[];
|
|
64
|
+
};
|
|
65
|
+
camera?: {
|
|
66
|
+
capture?: 'photo' | 'video' | 'both';
|
|
67
|
+
};
|
|
68
|
+
[key: string]: unknown;
|
|
69
|
+
}
|
|
70
|
+
export type FileStatus = 'queued' | 'uploading' | 'completing' | 'done' | 'failed';
|
|
71
|
+
export type SubmitStage = 'contract' | 'validate' | 'upload' | 'captcha' | 'submit' | 'done';
|
|
72
|
+
export interface FileProgress {
|
|
73
|
+
field: string;
|
|
74
|
+
name: string;
|
|
75
|
+
size: number;
|
|
76
|
+
status: FileStatus;
|
|
77
|
+
/** 0 to 100 */
|
|
78
|
+
percent: number;
|
|
79
|
+
/** The `_system_files` id once the file is uploaded and completed. */
|
|
80
|
+
fileId?: string;
|
|
81
|
+
}
|
|
82
|
+
export interface SubmitProgress {
|
|
83
|
+
stage: SubmitStage;
|
|
84
|
+
/** Overall, 0 to 100: bytes uploaded, then the submit. */
|
|
85
|
+
percent: number;
|
|
86
|
+
files: FileProgress[];
|
|
87
|
+
}
|
|
88
|
+
export interface SubmitInput {
|
|
89
|
+
/** Every non-file field, by field key. */
|
|
90
|
+
values: Record<string, unknown>;
|
|
91
|
+
/** File fields: one File, or an array for multi-file fields. */
|
|
92
|
+
files?: Record<string, File | File[] | null | undefined>;
|
|
93
|
+
onProgress?: (progress: SubmitProgress) => void;
|
|
94
|
+
signal?: AbortSignal;
|
|
95
|
+
}
|
|
96
|
+
export type PublicRecord = Record<string, unknown>;
|
|
97
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,qGAAqG;AACrG,MAAM,MAAM,OAAO,GAAG,CAAC,OAAO,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,IAAI,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAEjK,MAAM,WAAW,gBAAgB;IAC/B,6FAA6F;IAC7F,OAAO,EAAE,MAAM,CAAC;IAChB,wDAAwD;IACxD,MAAM,EAAE,MAAM,CAAC;IACf,2FAA2F;IAC3F,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kDAAkD;IAClD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6FAA6F;IAC7F,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,+DAA+D;IAC/D,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,gHAAgH;IAChH,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACvD,+DAA+D;IAC/D,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB,8FAA8F;IAC9F,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,uFAAuF;AACvF,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,mBAAmB,EAAE,CAAC;IAC9B,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAC;IACtE,MAAM,EAAE;QACN,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC;QACzB,GAAG,EAAE;YAAE,MAAM,EAAE;gBAAE,OAAO,EAAE,OAAO,CAAC;gBAAC,SAAS,EAAE,MAAM,EAAE,CAAC;gBAAC,eAAe,EAAE,OAAO,CAAA;aAAE,CAAA;SAAE,CAAC;QACrF,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KAClC,CAAC;CACH;AAED,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IACvE,MAAM,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,GAAG,MAAM,CAAA;KAAE,CAAC;IAClD,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,WAAW,GAAG,YAAY,GAAG,MAAM,GAAG,QAAQ,CAAC;AACnF,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,UAAU,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,MAAM,CAAC;AAE7F,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,UAAU,CAAC;IACnB,eAAe;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,WAAW,CAAC;IACnB,0DAA0D;IAC1D,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,YAAY,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,WAAW;IAC1B,0CAA0C;IAC1C,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,GAAG,IAAI,EAAE,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IACzD,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,cAAc,KAAK,IAAI,CAAC;IAChD,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PublicContractField, PublicViewContract } from './types.js';
|
|
2
|
+
/** Field types whose value is a list of uploaded file ids (the same set the server accepts public uploads for). */
|
|
3
|
+
export declare const uploadFieldTypes: readonly ["file", "image", "camera", "video", "signature"];
|
|
4
|
+
export declare function isUploadField(field: PublicContractField): boolean;
|
|
5
|
+
export declare function contentTypeOf(file: File): string;
|
|
6
|
+
/** The create fields that take files, by key. */
|
|
7
|
+
export declare function uploadFields(contract: PublicViewContract): Map<string, PublicContractField>;
|
|
8
|
+
/** Checks the chosen files against each field's rules before anything is uploaded. The server enforces the same rules. */
|
|
9
|
+
export declare function validateFiles(contract: PublicViewContract, files: Record<string, File[]>): void;
|
|
10
|
+
//# sourceMappingURL=validate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAE1E,mHAAmH;AACnH,eAAO,MAAM,gBAAgB,4DAA6D,CAAC;AAE3F,wBAAgB,aAAa,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAEjE;AAOD,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAEhD;AAED,iDAAiD;AACjD,wBAAgB,YAAY,CAAC,QAAQ,EAAE,kBAAkB,GAAG,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAG3F;AAED,0HAA0H;AAC1H,wBAAgB,aAAa,CAAC,QAAQ,EAAE,kBAAkB,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,GAAG,IAAI,CAsB/F"}
|
package/dist/validate.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { PublicFormError } from './errors.js';
|
|
2
|
+
/** Field types whose value is a list of uploaded file ids (the same set the server accepts public uploads for). */
|
|
3
|
+
export const uploadFieldTypes = ['file', 'image', 'camera', 'video', 'signature'];
|
|
4
|
+
export function isUploadField(field) {
|
|
5
|
+
return uploadFieldTypes.includes(field.type);
|
|
6
|
+
}
|
|
7
|
+
/** `*` / `type/*` / exact type, the same matching the server applies to a field's accept list. */
|
|
8
|
+
function accepts(contentType, accept) {
|
|
9
|
+
return accept === '*/*' || accept === contentType || (accept.endsWith('/*') && contentType.startsWith(accept.slice(0, -1)));
|
|
10
|
+
}
|
|
11
|
+
export function contentTypeOf(file) {
|
|
12
|
+
return file.type || 'application/octet-stream';
|
|
13
|
+
}
|
|
14
|
+
/** The create fields that take files, by key. */
|
|
15
|
+
export function uploadFields(contract) {
|
|
16
|
+
const keys = new Set(contract.public.api.create.fieldKeys);
|
|
17
|
+
return new Map(contract.fields.filter((field) => isUploadField(field) && (!keys.size || keys.has(field.name))).map((field) => [field.name, field]));
|
|
18
|
+
}
|
|
19
|
+
/** Checks the chosen files against each field's rules before anything is uploaded. The server enforces the same rules. */
|
|
20
|
+
export function validateFiles(contract, files) {
|
|
21
|
+
const fields = uploadFields(contract);
|
|
22
|
+
for (const [key, list] of Object.entries(files)) {
|
|
23
|
+
if (!list.length)
|
|
24
|
+
continue;
|
|
25
|
+
const field = fields.get(key);
|
|
26
|
+
if (!field)
|
|
27
|
+
throw new PublicFormError(`"${key}" is not a file field of this form.`, 'validate', { field: key });
|
|
28
|
+
const maxFiles = field.file?.maxFiles ?? 1;
|
|
29
|
+
if (list.length > maxFiles)
|
|
30
|
+
throw new PublicFormError(`${field.label} accepts at most ${maxFiles} file${maxFiles === 1 ? '' : 's'}.`, 'validate', { field: key });
|
|
31
|
+
for (const file of list) {
|
|
32
|
+
const fail = (message) => new PublicFormError(`${field.label}: ${file.name} ${message}`, 'validate', { field: key, fileName: file.name });
|
|
33
|
+
const type = contentTypeOf(file).toLowerCase();
|
|
34
|
+
if (!file.size)
|
|
35
|
+
throw fail('is empty.');
|
|
36
|
+
if ((field.type === 'image' || field.type === 'signature') && !type.startsWith('image/'))
|
|
37
|
+
throw fail('is not an image.');
|
|
38
|
+
if (field.type === 'video' && !type.startsWith('video/'))
|
|
39
|
+
throw fail('is not a video.');
|
|
40
|
+
if (field.type === 'camera') {
|
|
41
|
+
const capture = field.camera?.capture ?? 'photo';
|
|
42
|
+
if (!((capture !== 'video' && type.startsWith('image/')) || (capture !== 'photo' && type.startsWith('video/'))))
|
|
43
|
+
throw fail(`is not allowed (${capture === 'both' ? 'photos and videos' : capture === 'video' ? 'videos' : 'photos'} only).`);
|
|
44
|
+
}
|
|
45
|
+
if (field.file?.maxSizeBytes && file.size > field.file.maxSizeBytes)
|
|
46
|
+
throw fail(`is larger than ${Math.round(field.file.maxSizeBytes / 1024 / 1024 * 100) / 100} MB.`);
|
|
47
|
+
if (field.file?.accept?.length && !field.file.accept.some((accept) => accepts(type, accept)))
|
|
48
|
+
throw fail(`has a type that is not allowed (${type}).`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=validate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.js","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,70 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@layrstack/public-client",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Browser client for LayrStack public forms: uploads files (intent, direct storage PUT, complete) and submits the record in one call, with LayrStack reCAPTCHA support. Includes an optional Angular wrapper.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"layrstack",
|
|
7
|
+
"public-form",
|
|
8
|
+
"file-upload",
|
|
9
|
+
"angular",
|
|
10
|
+
"recaptcha"
|
|
11
|
+
],
|
|
12
|
+
"license": "UNLICENSED",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"sideEffects": false,
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"default": "./dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./angular": {
|
|
23
|
+
"types": "./dist/angular/index.d.ts",
|
|
24
|
+
"default": "./dist/angular/index.js"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"README.md"
|
|
30
|
+
],
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc --project tsconfig.build.json",
|
|
36
|
+
"lint": "tsc --noEmit --project tsconfig.json && tsc --noEmit --project tsconfig.samples.json",
|
|
37
|
+
"test": "tsc --project tsconfig.test.json && node --test .test-out/test/",
|
|
38
|
+
"harness": "pnpm build && node harness/serve.mjs",
|
|
39
|
+
"pack": "pnpm build && pnpm pack",
|
|
40
|
+
"prepublishOnly": "npm test && npm run build"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"@angular/core": ">=17.0.0",
|
|
44
|
+
"rxjs": ">=7.0.0"
|
|
45
|
+
},
|
|
46
|
+
"peerDependenciesMeta": {
|
|
47
|
+
"@angular/core": {
|
|
48
|
+
"optional": true
|
|
49
|
+
},
|
|
50
|
+
"rxjs": {
|
|
51
|
+
"optional": true
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@angular/common": "21.2.23",
|
|
56
|
+
"@angular/core": "21.2.23",
|
|
57
|
+
"@types/node": "^22.10.2",
|
|
58
|
+
"rxjs": "^7.8.1",
|
|
59
|
+
"typescript": "~5.9.3"
|
|
60
|
+
},
|
|
61
|
+
"repository": {
|
|
62
|
+
"type": "git",
|
|
63
|
+
"url": "git+https://github.com/Oxvsys/layrstack-v2.git",
|
|
64
|
+
"directory": "packages/public-client"
|
|
65
|
+
},
|
|
66
|
+
"homepage": "https://github.com/Oxvsys/layrstack-v2/tree/main/packages/public-client#readme",
|
|
67
|
+
"bugs": {
|
|
68
|
+
"url": "https://github.com/Oxvsys/layrstack-v2/issues"
|
|
69
|
+
}
|
|
70
|
+
}
|