@aglyn/plugins-forms 1.0.0-beta.220 → 1.0.0-beta.222

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.
Files changed (75) hide show
  1. package/README.md +1 -0
  2. package/package.json +14 -12
  3. package/src/lib/components/form-detail-card.js +166 -29
  4. package/src/lib/components/form-detail-card.js.map +1 -1
  5. package/src/lib/components/form-submissions-card.component.d.ts +12 -0
  6. package/src/lib/components/form-submissions-card.component.js +35 -2
  7. package/src/lib/components/form-submissions-card.component.js.map +1 -1
  8. package/src/lib/components/form.js +2 -1
  9. package/src/lib/components/form.js.map +1 -1
  10. package/src/lib/components/host-forms-card.component.d.ts +8 -0
  11. package/src/lib/components/host-forms-card.component.js +26 -1
  12. package/src/lib/components/host-forms-card.component.js.map +1 -1
  13. package/src/lib/components/site-package-form-preview.component.d.ts +13 -0
  14. package/src/lib/components/site-package-form-preview.component.js +57 -0
  15. package/src/lib/components/site-package-form-preview.component.js.map +1 -0
  16. package/src/lib/constants/site-package.d.ts +18 -0
  17. package/src/lib/constants/site-package.js +18 -0
  18. package/src/lib/constants/site-package.js.map +1 -0
  19. package/src/lib/declarations.console-server.d.ts +39 -0
  20. package/src/lib/declarations.console-server.js +68 -0
  21. package/src/lib/declarations.console-server.js.map +1 -0
  22. package/src/lib/declarations.server.js +22 -0
  23. package/src/lib/declarations.server.js.map +1 -1
  24. package/src/lib/model/form-door.d.ts +37 -0
  25. package/src/lib/model/form-door.js +50 -0
  26. package/src/lib/model/form-door.js.map +1 -0
  27. package/src/lib/model/form-submission-counts.d.ts +26 -0
  28. package/src/lib/model/form-submission-counts.js +35 -0
  29. package/src/lib/model/form-submission-counts.js.map +1 -0
  30. package/src/lib/model/form-submission-list.d.ts +40 -0
  31. package/src/lib/model/form-submission-list.js +66 -0
  32. package/src/lib/model/form-submission-list.js.map +1 -0
  33. package/src/lib/model/form-submission-record.d.ts +50 -0
  34. package/src/lib/model/form-submission-record.js +68 -0
  35. package/src/lib/model/form-submission-record.js.map +1 -0
  36. package/src/lib/model/form-unavailable.d.ts +83 -0
  37. package/src/lib/model/form-unavailable.js +88 -0
  38. package/src/lib/model/form-unavailable.js.map +1 -0
  39. package/src/lib/model/forms-record-lists.d.ts +31 -0
  40. package/src/lib/model/forms-record-lists.js +50 -0
  41. package/src/lib/model/forms-record-lists.js.map +1 -0
  42. package/src/lib/plan-entitlements.js +6 -6
  43. package/src/lib/plan-entitlements.js.map +1 -1
  44. package/src/lib/plugin.js +34 -0
  45. package/src/lib/plugin.js.map +1 -1
  46. package/src/lib/server/api-v1/form-submissions.d.ts +34 -0
  47. package/src/lib/server/api-v1/form-submissions.js +273 -0
  48. package/src/lib/server/api-v1/form-submissions.js.map +1 -0
  49. package/src/lib/server/api-v1/openapi.d.ts +26 -0
  50. package/src/lib/server/api-v1/openapi.js +133 -0
  51. package/src/lib/server/api-v1/openapi.js.map +1 -0
  52. package/src/lib/server/form-intake-gate.d.ts +34 -0
  53. package/src/lib/server/form-intake-gate.js +44 -0
  54. package/src/lib/server/form-intake-gate.js.map +1 -0
  55. package/src/lib/server/form-submission-index.d.ts +26 -0
  56. package/src/lib/server/form-submission-index.js +51 -0
  57. package/src/lib/server/form-submission-index.js.map +1 -0
  58. package/src/lib/server/form-submit.d.ts +43 -0
  59. package/src/lib/server/form-submit.js +769 -0
  60. package/src/lib/server/form-submit.js.map +1 -0
  61. package/src/lib/server/increment-form-stats.d.ts +70 -0
  62. package/src/lib/server/increment-form-stats.js +112 -0
  63. package/src/lib/server/increment-form-stats.js.map +1 -0
  64. package/src/lib/server.d.ts +10 -0
  65. package/src/lib/server.js +15 -0
  66. package/src/lib/server.js.map +1 -1
  67. package/src/lib/transfer/form-submissions-transfer-key.d.ts +37 -0
  68. package/src/lib/transfer/form-submissions-transfer-key.js +40 -0
  69. package/src/lib/transfer/form-submissions-transfer-key.js.map +1 -0
  70. package/src/lib/transfer/form-submissions-transfer.d.ts +98 -0
  71. package/src/lib/transfer/form-submissions-transfer.js +563 -0
  72. package/src/lib/transfer/form-submissions-transfer.js.map +1 -0
  73. package/src/lib/transfer/form-submissions-transfer.server.d.ts +26 -0
  74. package/src/lib/transfer/form-submissions-transfer.server.js +31 -0
  75. package/src/lib/transfer/form-submissions-transfer.server.js.map +1 -0
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * What `/api/forms/submit` answers a submission to a site that switched Forms
19
+ * off (AGL-3029). A visitor reads it, so it names no plugin and no setting.
20
+ */
21
+ export declare const FORMS_OFF_FOR_SITE_REFUSAL = "This site is not accepting form submissions";
22
+ /**
23
+ * The plugin whose door a submission to `/api/forms/submit` came through
24
+ * (AGL-3029) — the plugin that must run on the site for it to be accepted.
25
+ *
26
+ * The endpoint is shared. A form element posts to it, and so does a Marketing
27
+ * popup's email capture, which is a Marketing element rather than a form: a
28
+ * site that switched Forms off and still shows its popup must not silently
29
+ * lose every address the popup collects. The popup names its door in the
30
+ * body; everything else is a form's.
31
+ *
32
+ * A body that names a form ENTITY or a dataset binding is a form's, whatever
33
+ * door it claims. Those are what a form element sends and a popup never does,
34
+ * so the popup door cannot be used to file rows under a form, or into a
35
+ * dataset, on a site that switched Forms off.
36
+ */
37
+ export declare function formSubmissionDoorPlugin(body: Record<string, unknown> | null | undefined): string;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ /**
17
+ * Which plugin's switch decides a submission to this plugin's door, and what
18
+ * the door says when that switch is off (AGL-3029). The door's own words:
19
+ * read by the door (`server/form-submit.ts`) and nothing else.
20
+ */ import { BUNDLE_ID } from "../constants/bundle-common.js";
21
+ /**
22
+ * What `/api/forms/submit` answers a submission to a site that switched Forms
23
+ * off (AGL-3029). A visitor reads it, so it names no plugin and no setting.
24
+ */ export const FORMS_OFF_FOR_SITE_REFUSAL = 'This site is not accepting form submissions';
25
+ /**
26
+ * The marketing plugin's id, as the door names the plugin whose switch judges
27
+ * a popup's capture. This plugin holds no import of it; the catalog is where
28
+ * the string is defined.
29
+ */ const MARKETING_PLUGIN_ID = 'marketing';
30
+ /**
31
+ * The plugin whose door a submission to `/api/forms/submit` came through
32
+ * (AGL-3029) — the plugin that must run on the site for it to be accepted.
33
+ *
34
+ * The endpoint is shared. A form element posts to it, and so does a Marketing
35
+ * popup's email capture, which is a Marketing element rather than a form: a
36
+ * site that switched Forms off and still shows its popup must not silently
37
+ * lose every address the popup collects. The popup names its door in the
38
+ * body; everything else is a form's.
39
+ *
40
+ * A body that names a form ENTITY or a dataset binding is a form's, whatever
41
+ * door it claims. Those are what a form element sends and a popup never does,
42
+ * so the popup door cannot be used to file rows under a form, or into a
43
+ * dataset, on a site that switched Forms off.
44
+ */ export function formSubmissionDoorPlugin(body) {
45
+ var _ref;
46
+ const namesFormData = Boolean(String((_ref = body == null ? void 0 : body['formId']) != null ? _ref : '').trim()) || Boolean(body == null ? void 0 : body['datasetBinding']);
47
+ return (body == null ? void 0 : body['door']) === 'popup' && !namesFormData ? MARKETING_PLUGIN_ID : BUNDLE_ID;
48
+ }
49
+
50
+ //# sourceMappingURL=form-door.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/forms/src/lib/model/form-door.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Which plugin's switch decides a submission to this plugin's door, and what\n * the door says when that switch is off (AGL-3029). The door's own words:\n * read by the door (`server/form-submit.ts`) and nothing else.\n */\n\nimport { BUNDLE_ID } from '../constants/bundle-common'\n\n/**\n * What `/api/forms/submit` answers a submission to a site that switched Forms\n * off (AGL-3029). A visitor reads it, so it names no plugin and no setting.\n */\nexport const FORMS_OFF_FOR_SITE_REFUSAL = 'This site is not accepting form submissions'\n\n/**\n * The marketing plugin's id, as the door names the plugin whose switch judges\n * a popup's capture. This plugin holds no import of it; the catalog is where\n * the string is defined.\n */\nconst MARKETING_PLUGIN_ID = 'marketing'\n\n/**\n * The plugin whose door a submission to `/api/forms/submit` came through\n * (AGL-3029) — the plugin that must run on the site for it to be accepted.\n *\n * The endpoint is shared. A form element posts to it, and so does a Marketing\n * popup's email capture, which is a Marketing element rather than a form: a\n * site that switched Forms off and still shows its popup must not silently\n * lose every address the popup collects. The popup names its door in the\n * body; everything else is a form's.\n *\n * A body that names a form ENTITY or a dataset binding is a form's, whatever\n * door it claims. Those are what a form element sends and a popup never does,\n * so the popup door cannot be used to file rows under a form, or into a\n * dataset, on a site that switched Forms off.\n */\nexport function formSubmissionDoorPlugin(body: Record<string, unknown> | null | undefined): string {\n const namesFormData =\n Boolean(String(body?.['formId'] ?? '').trim()) || Boolean(body?.['datasetBinding'])\n return body?.['door'] === 'popup' && !namesFormData ? MARKETING_PLUGIN_ID : BUNDLE_ID\n}\n"],"names":["BUNDLE_ID","FORMS_OFF_FOR_SITE_REFUSAL","MARKETING_PLUGIN_ID","formSubmissionDoorPlugin","body","namesFormData","Boolean","String","trim"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;CAIC,GAED,SAASA,SAAS,QAAQ,gCAA4B;AAEtD;;;CAGC,GACD,OAAO,MAAMC,6BAA6B,8CAA6C;AAEvF;;;;CAIC,GACD,MAAMC,sBAAsB;AAE5B;;;;;;;;;;;;;;CAcC,GACD,OAAO,SAASC,yBAAyBC,IAAgD;;IACvF,MAAMC,gBACJC,QAAQC,eAAOH,wBAAAA,IAAM,CAAC,SAAS,mBAAI,IAAII,IAAI,OAAOF,QAAQF,wBAAAA,IAAM,CAAC,iBAAiB;IACpF,OAAOA,CAAAA,wBAAAA,IAAM,CAAC,OAAO,MAAK,WAAW,CAACC,gBAAgBH,sBAAsBF;AAC9E"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type PluginRecordCountSource } from '@aglyn/aglyn/plugin-manager/plugin-record-counts';
18
+ /**
19
+ * How many submissions a site received, counted in the console for another
20
+ * plugin's figure (AGL-3080) — a campaign's conversions out of every
21
+ * submission the site took in. Every one is the site's own, so the count
22
+ * never crosses sites; a reader with no site counts none.
23
+ */
24
+ export declare const formSubmissionRecordCountSource: PluginRecordCountSource;
25
+ /** Called from the console registrar, owner named for a spec that calls it directly. */
26
+ export declare function registerFormSubmissionCounts(): void;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { registerPluginRecordCountSource } from "@aglyn/aglyn/plugin-manager/plugin-record-counts";
17
+ import { collection } from "firebase/firestore";
18
+ import { BUNDLE_ID } from "../constants/bundle-common.js";
19
+ /**
20
+ * How many submissions a site received, counted in the console for another
21
+ * plugin's figure (AGL-3080) — a campaign's conversions out of every
22
+ * submission the site took in. Every one is the site's own, so the count
23
+ * never crosses sites; a reader with no site counts none.
24
+ */ export const formSubmissionRecordCountSource = {
25
+ query (firestore, request) {
26
+ return request.hostId ? collection(firestore, 'hosts', request.hostId, 'formSubmissions') : null;
27
+ }
28
+ };
29
+ /** Called from the console registrar, owner named for a spec that calls it directly. */ export function registerFormSubmissionCounts() {
30
+ registerPluginRecordCountSource('formSubmission', formSubmissionRecordCountSource, {
31
+ pluginId: BUNDLE_ID
32
+ });
33
+ }
34
+
35
+ //# sourceMappingURL=form-submission-counts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/forms/src/lib/model/form-submission-counts.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n registerPluginRecordCountSource,\n type PluginRecordCountSource,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-counts'\nimport { collection } from 'firebase/firestore'\nimport { BUNDLE_ID } from '../constants/bundle-common'\n\n/**\n * How many submissions a site received, counted in the console for another\n * plugin's figure (AGL-3080) — a campaign's conversions out of every\n * submission the site took in. Every one is the site's own, so the count\n * never crosses sites; a reader with no site counts none.\n */\nexport const formSubmissionRecordCountSource: PluginRecordCountSource = {\n query(firestore, request) {\n return request.hostId ? collection(firestore, 'hosts', request.hostId, 'formSubmissions') : null\n },\n}\n\n/** Called from the console registrar, owner named for a spec that calls it directly. */\nexport function registerFormSubmissionCounts(): void {\n registerPluginRecordCountSource('formSubmission', formSubmissionRecordCountSource, {\n pluginId: BUNDLE_ID,\n })\n}\n"],"names":["registerPluginRecordCountSource","collection","BUNDLE_ID","formSubmissionRecordCountSource","query","firestore","request","hostId","registerFormSubmissionCounts","pluginId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,+BAA+B,QAE1B,mDAAkD;AACzD,SAASC,UAAU,QAAQ,qBAAoB;AAC/C,SAASC,SAAS,QAAQ,gCAA4B;AAEtD;;;;;CAKC,GACD,OAAO,MAAMC,kCAA2D;IACtEC,OAAMC,SAAS,EAAEC,OAAO;QACtB,OAAOA,QAAQC,MAAM,GAAGN,WAAWI,WAAW,SAASC,QAAQC,MAAM,EAAE,qBAAqB;IAC9F;AACF,EAAC;AAED,sFAAsF,GACtF,OAAO,SAASC;IACdR,gCAAgC,kBAAkBG,iCAAiC;QACjFM,UAAUP;IACZ;AACF"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type PluginRecordListSource } from '@aglyn/aglyn/plugin-manager/plugin-record-lists';
18
+ /**
19
+ * A site's form submissions, listed in the console for the plugin that
20
+ * reads them (AGL-3080) — the Inbox. The record is
21
+ * `formSubmissionIndexedRecord`'s, and the stored fields a reader may filter,
22
+ * order and change are documented there.
23
+ *
24
+ * - `query` — the newest of one site's, at most `limit`, for a glance.
25
+ * - `walk` — the base a paged list walks: a site's submissions, or on the
26
+ * organization's Inbox every site's, which the reader narrows to the
27
+ * organization by `orgId` — the only group read the rules admit. The
28
+ * reader's list query adds that, its filters, its `createdAt` order and
29
+ * its pages.
30
+ * - `doc` — one site's submission, which the Inbox opens, marks read or
31
+ * unread, and deletes (a site admin's or editor's to do, by the rules), and
32
+ * under which it reads the replies it sent.
33
+ * - `byIds` — a site's submissions by name, at most
34
+ * {@link PLUGIN_RECORD_LIST_IDS_MAX} at a time, for a reader that holds ids
35
+ * from elsewhere: a campaign's conversion report grouping the submissions it
36
+ * was credited with by the page each was sent from.
37
+ */
38
+ export declare const formSubmissionListSource: PluginRecordListSource;
39
+ /** Called from the console registrar, owner named for a spec that calls it directly. */
40
+ export declare function registerFormSubmissionList(): void;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { PLUGIN_RECORD_LIST_IDS_MAX, registerPluginRecordListSource } from "@aglyn/aglyn/plugin-manager/plugin-record-lists";
17
+ import { collection, collectionGroup, doc, documentId, limit, orderBy, query, where } from "firebase/firestore";
18
+ import { BUNDLE_ID } from "../constants/bundle-common.js";
19
+ import { formSubmissionIndexedRecord } from "./form-submission-record.js";
20
+ /** The collection this plugin keeps submissions in, under a site. */ const SUBMISSIONS = 'formSubmissions';
21
+ /**
22
+ * A site's form submissions, listed in the console for the plugin that
23
+ * reads them (AGL-3080) — the Inbox. The record is
24
+ * `formSubmissionIndexedRecord`'s, and the stored fields a reader may filter,
25
+ * order and change are documented there.
26
+ *
27
+ * - `query` — the newest of one site's, at most `limit`, for a glance.
28
+ * - `walk` — the base a paged list walks: a site's submissions, or on the
29
+ * organization's Inbox every site's, which the reader narrows to the
30
+ * organization by `orgId` — the only group read the rules admit. The
31
+ * reader's list query adds that, its filters, its `createdAt` order and
32
+ * its pages.
33
+ * - `doc` — one site's submission, which the Inbox opens, marks read or
34
+ * unread, and deletes (a site admin's or editor's to do, by the rules), and
35
+ * under which it reads the replies it sent.
36
+ * - `byIds` — a site's submissions by name, at most
37
+ * {@link PLUGIN_RECORD_LIST_IDS_MAX} at a time, for a reader that holds ids
38
+ * from elsewhere: a campaign's conversion report grouping the submissions it
39
+ * was credited with by the page each was sent from.
40
+ */ export const formSubmissionListSource = {
41
+ query (firestore, request) {
42
+ if (!request.hostId || request.search || request.installedFrom) return null;
43
+ return query(collection(firestore, 'hosts', request.hostId, SUBMISSIONS), orderBy('createdAt', 'desc'), limit(request.limit));
44
+ },
45
+ walk (firestore, request) {
46
+ if (request.hostId) return collection(firestore, 'hosts', request.hostId, SUBMISSIONS);
47
+ return request.orgId ? collectionGroup(firestore, SUBMISSIONS) : null;
48
+ },
49
+ doc (firestore, request) {
50
+ if (!request.hostId || !request.id) return null;
51
+ return doc(firestore, 'hosts', request.hostId, SUBMISSIONS, request.id);
52
+ },
53
+ byIds (firestore, request) {
54
+ const ids = request.ids.filter(Boolean).slice(0, PLUGIN_RECORD_LIST_IDS_MAX);
55
+ if (!request.hostId || !ids.length) return null;
56
+ return query(collection(firestore, 'hosts', request.hostId, SUBMISSIONS), where(documentId(), 'in', ids));
57
+ },
58
+ record: formSubmissionIndexedRecord
59
+ };
60
+ /** Called from the console registrar, owner named for a spec that calls it directly. */ export function registerFormSubmissionList() {
61
+ registerPluginRecordListSource('formSubmission', formSubmissionListSource, {
62
+ pluginId: BUNDLE_ID
63
+ });
64
+ }
65
+
66
+ //# sourceMappingURL=form-submission-list.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/forms/src/lib/model/form-submission-list.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n PLUGIN_RECORD_LIST_IDS_MAX,\n registerPluginRecordListSource,\n type PluginRecordListSource,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-lists'\nimport {\n collection,\n collectionGroup,\n doc,\n documentId,\n limit,\n orderBy,\n query,\n where,\n} from 'firebase/firestore'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { formSubmissionIndexedRecord } from './form-submission-record'\n\n/** The collection this plugin keeps submissions in, under a site. */\nconst SUBMISSIONS = 'formSubmissions'\n\n/**\n * A site's form submissions, listed in the console for the plugin that\n * reads them (AGL-3080) — the Inbox. The record is\n * `formSubmissionIndexedRecord`'s, and the stored fields a reader may filter,\n * order and change are documented there.\n *\n * - `query` — the newest of one site's, at most `limit`, for a glance.\n * - `walk` — the base a paged list walks: a site's submissions, or on the\n * organization's Inbox every site's, which the reader narrows to the\n * organization by `orgId` — the only group read the rules admit. The\n * reader's list query adds that, its filters, its `createdAt` order and\n * its pages.\n * - `doc` — one site's submission, which the Inbox opens, marks read or\n * unread, and deletes (a site admin's or editor's to do, by the rules), and\n * under which it reads the replies it sent.\n * - `byIds` — a site's submissions by name, at most\n * {@link PLUGIN_RECORD_LIST_IDS_MAX} at a time, for a reader that holds ids\n * from elsewhere: a campaign's conversion report grouping the submissions it\n * was credited with by the page each was sent from.\n */\nexport const formSubmissionListSource: PluginRecordListSource = {\n query(firestore, request) {\n if (!request.hostId || request.search || request.installedFrom) return null\n return query(\n collection(firestore, 'hosts', request.hostId, SUBMISSIONS),\n orderBy('createdAt', 'desc'),\n limit(request.limit),\n )\n },\n walk(firestore, request) {\n if (request.hostId) return collection(firestore, 'hosts', request.hostId, SUBMISSIONS)\n return request.orgId ? collectionGroup(firestore, SUBMISSIONS) : null\n },\n doc(firestore, request) {\n if (!request.hostId || !request.id) return null\n return doc(firestore, 'hosts', request.hostId, SUBMISSIONS, request.id)\n },\n byIds(firestore, request) {\n const ids = request.ids.filter(Boolean).slice(0, PLUGIN_RECORD_LIST_IDS_MAX)\n if (!request.hostId || !ids.length) return null\n return query(\n collection(firestore, 'hosts', request.hostId, SUBMISSIONS),\n where(documentId(), 'in', ids),\n )\n },\n record: formSubmissionIndexedRecord,\n}\n\n/** Called from the console registrar, owner named for a spec that calls it directly. */\nexport function registerFormSubmissionList(): void {\n registerPluginRecordListSource('formSubmission', formSubmissionListSource, {\n pluginId: BUNDLE_ID,\n })\n}\n"],"names":["PLUGIN_RECORD_LIST_IDS_MAX","registerPluginRecordListSource","collection","collectionGroup","doc","documentId","limit","orderBy","query","where","BUNDLE_ID","formSubmissionIndexedRecord","SUBMISSIONS","formSubmissionListSource","firestore","request","hostId","search","installedFrom","walk","orgId","id","byIds","ids","filter","Boolean","slice","length","record","registerFormSubmissionList","pluginId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,0BAA0B,EAC1BC,8BAA8B,QAEzB,kDAAiD;AACxD,SACEC,UAAU,EACVC,eAAe,EACfC,GAAG,EACHC,UAAU,EACVC,KAAK,EACLC,OAAO,EACPC,KAAK,EACLC,KAAK,QACA,qBAAoB;AAC3B,SAASC,SAAS,QAAQ,gCAA4B;AACtD,SAASC,2BAA2B,QAAQ,8BAA0B;AAEtE,mEAAmE,GACnE,MAAMC,cAAc;AAEpB;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,MAAMC,2BAAmD;IAC9DL,OAAMM,SAAS,EAAEC,OAAO;QACtB,IAAI,CAACA,QAAQC,MAAM,IAAID,QAAQE,MAAM,IAAIF,QAAQG,aAAa,EAAE,OAAO;QACvE,OAAOV,MACLN,WAAWY,WAAW,SAASC,QAAQC,MAAM,EAAEJ,cAC/CL,QAAQ,aAAa,SACrBD,MAAMS,QAAQT,KAAK;IAEvB;IACAa,MAAKL,SAAS,EAAEC,OAAO;QACrB,IAAIA,QAAQC,MAAM,EAAE,OAAOd,WAAWY,WAAW,SAASC,QAAQC,MAAM,EAAEJ;QAC1E,OAAOG,QAAQK,KAAK,GAAGjB,gBAAgBW,WAAWF,eAAe;IACnE;IACAR,KAAIU,SAAS,EAAEC,OAAO;QACpB,IAAI,CAACA,QAAQC,MAAM,IAAI,CAACD,QAAQM,EAAE,EAAE,OAAO;QAC3C,OAAOjB,IAAIU,WAAW,SAASC,QAAQC,MAAM,EAAEJ,aAAaG,QAAQM,EAAE;IACxE;IACAC,OAAMR,SAAS,EAAEC,OAAO;QACtB,MAAMQ,MAAMR,QAAQQ,GAAG,CAACC,MAAM,CAACC,SAASC,KAAK,CAAC,GAAG1B;QACjD,IAAI,CAACe,QAAQC,MAAM,IAAI,CAACO,IAAII,MAAM,EAAE,OAAO;QAC3C,OAAOnB,MACLN,WAAWY,WAAW,SAASC,QAAQC,MAAM,EAAEJ,cAC/CH,MAAMJ,cAAc,MAAMkB;IAE9B;IACAK,QAAQjB;AACV,EAAC;AAED,sFAAsF,GACtF,OAAO,SAASkB;IACd5B,+BAA+B,kBAAkBY,0BAA0B;QACzEiB,UAAUpB;IACZ;AACF"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { PluginIndexedRecord } from '@aglyn/aglyn/plugin-manager/plugin-record-index';
18
+ /**
19
+ * A FORM SUBMISSION, as this plugin shares it with another (AGL-3080).
20
+ *
21
+ * Submissions are this plugin's records: its door (`/api/forms/submit`)
22
+ * writes every one, its `/v1` resource serves them, its counters are what the
23
+ * plan bills, and they live where it keeps them —
24
+ * `hosts/{hostId}/formSubmissions/{id}`, which only this plugin names. The
25
+ * Inbox is the console's reader of them, and reads, marks and threads them
26
+ * through what this plugin publishes for the `formSubmission` kind: a list
27
+ * source in the console (`form-submission-list.ts`) and a record index on the
28
+ * server (`server/form-submission-index.ts`).
29
+ *
30
+ * ## The record, as a reader may rely on it
31
+ *
32
+ * Every stored document carries, from the moment the door writes it:
33
+ * `hostId`, and `orgId` when the site has one (both frozen by the rules);
34
+ * `formId` for a verified form entity; `formName` and `path`; `fields`, the
35
+ * values as typed, keyed by field name; `senderTokens` and `searchTokens`,
36
+ * the word-prefix keys a list filters and searches by; `read`, a boolean;
37
+ * `createdAt`, the server time it arrived; and `campaignIds`, `routing` and
38
+ * `rateDegraded` where they apply. A reader may change `read`, and stamp
39
+ * `repliedAtMs` with `read` when it answers one; it may keep its own records
40
+ * of what it did with one under the document (the Inbox's `replies` and
41
+ * `listAssignments`). Nothing else on it is a reader's to write, and the
42
+ * security rules hold the client half of that line.
43
+ *
44
+ * `null` for a document that holds nothing. Otherwise its id, a name — the
45
+ * form it was sent to, as it was called when it arrived — and `facts`:
46
+ * `hostId`, `formId` (`null` for a submission no form entity filed),
47
+ * `formName`, `path`, `fields`, `read` and `createdAtMs` (`null` before the
48
+ * server stamped it).
49
+ */
50
+ export declare function formSubmissionIndexedRecord(id: string, data: Readonly<Record<string, unknown>> | undefined): PluginIndexedRecord | null;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ /**
17
+ * A FORM SUBMISSION, as this plugin shares it with another (AGL-3080).
18
+ *
19
+ * Submissions are this plugin's records: its door (`/api/forms/submit`)
20
+ * writes every one, its `/v1` resource serves them, its counters are what the
21
+ * plan bills, and they live where it keeps them —
22
+ * `hosts/{hostId}/formSubmissions/{id}`, which only this plugin names. The
23
+ * Inbox is the console's reader of them, and reads, marks and threads them
24
+ * through what this plugin publishes for the `formSubmission` kind: a list
25
+ * source in the console (`form-submission-list.ts`) and a record index on the
26
+ * server (`server/form-submission-index.ts`).
27
+ *
28
+ * ## The record, as a reader may rely on it
29
+ *
30
+ * Every stored document carries, from the moment the door writes it:
31
+ * `hostId`, and `orgId` when the site has one (both frozen by the rules);
32
+ * `formId` for a verified form entity; `formName` and `path`; `fields`, the
33
+ * values as typed, keyed by field name; `senderTokens` and `searchTokens`,
34
+ * the word-prefix keys a list filters and searches by; `read`, a boolean;
35
+ * `createdAt`, the server time it arrived; and `campaignIds`, `routing` and
36
+ * `rateDegraded` where they apply. A reader may change `read`, and stamp
37
+ * `repliedAtMs` with `read` when it answers one; it may keep its own records
38
+ * of what it did with one under the document (the Inbox's `replies` and
39
+ * `listAssignments`). Nothing else on it is a reader's to write, and the
40
+ * security rules hold the client half of that line.
41
+ *
42
+ * `null` for a document that holds nothing. Otherwise its id, a name — the
43
+ * form it was sent to, as it was called when it arrived — and `facts`:
44
+ * `hostId`, `formId` (`null` for a submission no form entity filed),
45
+ * `formName`, `path`, `fields`, `read` and `createdAtMs` (`null` before the
46
+ * server stamped it).
47
+ */ export function formSubmissionIndexedRecord(id, data) {
48
+ if (!data) return null;
49
+ const text = (value)=>typeof value === 'string' ? value : '';
50
+ const formName = text(data['formName']);
51
+ const createdAt = data['createdAt'];
52
+ const createdAtMs = typeof (createdAt == null ? void 0 : createdAt.toMillis) === 'function' ? createdAt.toMillis() : null;
53
+ return {
54
+ id,
55
+ name: formName || 'Form',
56
+ facts: {
57
+ hostId: text(data['hostId']) || null,
58
+ formId: text(data['formId']) || null,
59
+ formName,
60
+ path: text(data['path']),
61
+ fields: data['fields'] && typeof data['fields'] === 'object' ? data['fields'] : {},
62
+ read: data['read'] === true,
63
+ createdAtMs
64
+ }
65
+ };
66
+ }
67
+
68
+ //# sourceMappingURL=form-submission-record.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/forms/src/lib/model/form-submission-record.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { PluginIndexedRecord } from '@aglyn/aglyn/plugin-manager/plugin-record-index'\n\n/**\n * A FORM SUBMISSION, as this plugin shares it with another (AGL-3080).\n *\n * Submissions are this plugin's records: its door (`/api/forms/submit`)\n * writes every one, its `/v1` resource serves them, its counters are what the\n * plan bills, and they live where it keeps them —\n * `hosts/{hostId}/formSubmissions/{id}`, which only this plugin names. The\n * Inbox is the console's reader of them, and reads, marks and threads them\n * through what this plugin publishes for the `formSubmission` kind: a list\n * source in the console (`form-submission-list.ts`) and a record index on the\n * server (`server/form-submission-index.ts`).\n *\n * ## The record, as a reader may rely on it\n *\n * Every stored document carries, from the moment the door writes it:\n * `hostId`, and `orgId` when the site has one (both frozen by the rules);\n * `formId` for a verified form entity; `formName` and `path`; `fields`, the\n * values as typed, keyed by field name; `senderTokens` and `searchTokens`,\n * the word-prefix keys a list filters and searches by; `read`, a boolean;\n * `createdAt`, the server time it arrived; and `campaignIds`, `routing` and\n * `rateDegraded` where they apply. A reader may change `read`, and stamp\n * `repliedAtMs` with `read` when it answers one; it may keep its own records\n * of what it did with one under the document (the Inbox's `replies` and\n * `listAssignments`). Nothing else on it is a reader's to write, and the\n * security rules hold the client half of that line.\n *\n * `null` for a document that holds nothing. Otherwise its id, a name — the\n * form it was sent to, as it was called when it arrived — and `facts`:\n * `hostId`, `formId` (`null` for a submission no form entity filed),\n * `formName`, `path`, `fields`, `read` and `createdAtMs` (`null` before the\n * server stamped it).\n */\nexport function formSubmissionIndexedRecord(\n id: string,\n data: Readonly<Record<string, unknown>> | undefined,\n): PluginIndexedRecord | null {\n if (!data) return null\n const text = (value: unknown): string => (typeof value === 'string' ? value : '')\n const formName = text(data['formName'])\n const createdAt = data['createdAt'] as { toMillis?: () => number } | null | undefined\n const createdAtMs =\n typeof createdAt?.toMillis === 'function' ? createdAt.toMillis() : null\n return {\n id,\n name: formName || 'Form',\n facts: {\n hostId: text(data['hostId']) || null,\n formId: text(data['formId']) || null,\n formName,\n path: text(data['path']),\n fields:\n data['fields'] && typeof data['fields'] === 'object' ? data['fields'] : {},\n read: data['read'] === true,\n createdAtMs,\n },\n }\n}\n"],"names":["formSubmissionIndexedRecord","id","data","text","value","formName","createdAt","createdAtMs","toMillis","name","facts","hostId","formId","path","fields","read"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAID;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+BC,GACD,OAAO,SAASA,4BACdC,EAAU,EACVC,IAAmD;IAEnD,IAAI,CAACA,MAAM,OAAO;IAClB,MAAMC,OAAO,CAACC,QAA4B,OAAOA,UAAU,WAAWA,QAAQ;IAC9E,MAAMC,WAAWF,KAAKD,IAAI,CAAC,WAAW;IACtC,MAAMI,YAAYJ,IAAI,CAAC,YAAY;IACnC,MAAMK,cACJ,QAAOD,6BAAAA,UAAWE,QAAQ,MAAK,aAAaF,UAAUE,QAAQ,KAAK;IACrE,OAAO;QACLP;QACAQ,MAAMJ,YAAY;QAClBK,OAAO;YACLC,QAAQR,KAAKD,IAAI,CAAC,SAAS,KAAK;YAChCU,QAAQT,KAAKD,IAAI,CAAC,SAAS,KAAK;YAChCG;YACAQ,MAAMV,KAAKD,IAAI,CAAC,OAAO;YACvBY,QACEZ,IAAI,CAAC,SAAS,IAAI,OAAOA,IAAI,CAAC,SAAS,KAAK,WAAWA,IAAI,CAAC,SAAS,GAAG,CAAC;YAC3Ea,MAAMb,IAAI,CAAC,OAAO,KAAK;YACvBK;QACF;IACF;AACF"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * What a VISITOR is told when a site's form is not accepting (AGL-1666).
19
+ *
20
+ * The door answers a flooded site's submissions with a refusal the form
21
+ * element recognises by its code, and the element shows the sentence below.
22
+ * The owner's half of the same moment — the count, the ceiling and when it
23
+ * lifts — is the forms plugin's `visitorDoors` declaration, which the Inbox
24
+ * and the staff pages read without loading this plugin. The two halves are
25
+ * constrained in opposite directions: the visitor is a stranger and must not
26
+ * be told why; the owner needs exactly that.
27
+ */
28
+ /**
29
+ * Refusal code carried by the abuse-ceiling 429 (AGL-1655).
30
+ *
31
+ * The Free plan's monthly wall answers 429 too, so the STATUS identifies
32
+ * nothing — this code is the whole discriminator, and one means "the customer
33
+ * needs a bigger plan" while the other means "this site is being flooded".
34
+ * Shared rather than restated so the producer and the consumer cannot drift.
35
+ */
36
+ export declare const FORM_ABUSE_CEILING_CODE = "form-abuse-ceiling";
37
+ /** What the visitor is shown when a site's form is not accepting. */
38
+ export interface FormUnavailableNotice {
39
+ /** The whole message. Fixed copy — never the server's terse `error`. */
40
+ message: string;
41
+ /**
42
+ * An alternative way to reach the site, when the site publishes one
43
+ * (`business.supportEmail`, the token whose own description is "where
44
+ * visitors should write for help"). Absent when it does not.
45
+ */
46
+ contact?: string;
47
+ }
48
+ /**
49
+ * The visitor's sentence.
50
+ *
51
+ * Three things it must not do, all of which the generic error branch did or
52
+ * a friendlier rewrite would:
53
+ *
54
+ * 1. **Not blame the visitor.** They filled in a form correctly. Nothing
55
+ * about their message, their address or their retry is the reason.
56
+ * 2. **Not explain.** "Unusual volume", "temporarily over its limit", even
57
+ * "this site is receiving too many messages" all tell a stranger
58
+ * something about the site owner's account that the owner never chose to
59
+ * publish. The refusal is the owner's problem to see, not the caller's.
60
+ * 3. **Not sound delivered.** "We'll get back to you", or anything ending
61
+ * in a thank-you, converts a lost lead into a person waiting for a reply
62
+ * that is never coming. So it says, in as many words, that the message
63
+ * was not sent.
64
+ *
65
+ * What is left is short on purpose. The one genuinely useful thing to add is
66
+ * a door that still opens, which is why `contact` rides along.
67
+ */
68
+ export declare const FORM_UNAVAILABLE_MESSAGE: string;
69
+ /**
70
+ * Does this refused submit mean "this site's form is not accepting", rather
71
+ * than "something broke"?
72
+ *
73
+ * Takes the BODY only, deliberately. The obvious signature — `(status, body)`,
74
+ * matching `parseLockdownRefusal` — invites a caller to gate on the status
75
+ * first, and the status is precisely what does not distinguish this refusal
76
+ * from the Free plan's monthly wall. A caller holding only this function
77
+ * cannot make that mistake.
78
+ *
79
+ * Returns `null` for every other body, so a caller's existing generic error
80
+ * branch keeps handling real failures. Dressing a 500 as a deliberate pause
81
+ * would be the same class of lie in the other direction.
82
+ */
83
+ export declare function parseFormUnavailableRefusal(body: unknown): FormUnavailableNotice | null;
@@ -0,0 +1,88 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ /**
18
+ * What a VISITOR is told when a site's form is not accepting (AGL-1666).
19
+ *
20
+ * The door answers a flooded site's submissions with a refusal the form
21
+ * element recognises by its code, and the element shows the sentence below.
22
+ * The owner's half of the same moment — the count, the ceiling and when it
23
+ * lifts — is the forms plugin's `visitorDoors` declaration, which the Inbox
24
+ * and the staff pages read without loading this plugin. The two halves are
25
+ * constrained in opposite directions: the visitor is a stranger and must not
26
+ * be told why; the owner needs exactly that.
27
+ */ /**
28
+ * Refusal code carried by the abuse-ceiling 429 (AGL-1655).
29
+ *
30
+ * The Free plan's monthly wall answers 429 too, so the STATUS identifies
31
+ * nothing — this code is the whole discriminator, and one means "the customer
32
+ * needs a bigger plan" while the other means "this site is being flooded".
33
+ * Shared rather than restated so the producer and the consumer cannot drift.
34
+ */ export const FORM_ABUSE_CEILING_CODE = 'form-abuse-ceiling';
35
+ /**
36
+ * The visitor's sentence.
37
+ *
38
+ * Three things it must not do, all of which the generic error branch did or
39
+ * a friendlier rewrite would:
40
+ *
41
+ * 1. **Not blame the visitor.** They filled in a form correctly. Nothing
42
+ * about their message, their address or their retry is the reason.
43
+ * 2. **Not explain.** "Unusual volume", "temporarily over its limit", even
44
+ * "this site is receiving too many messages" all tell a stranger
45
+ * something about the site owner's account that the owner never chose to
46
+ * publish. The refusal is the owner's problem to see, not the caller's.
47
+ * 3. **Not sound delivered.** "We'll get back to you", or anything ending
48
+ * in a thank-you, converts a lost lead into a person waiting for a reply
49
+ * that is never coming. So it says, in as many words, that the message
50
+ * was not sent.
51
+ *
52
+ * What is left is short on purpose. The one genuinely useful thing to add is
53
+ * a door that still opens, which is why `contact` rides along.
54
+ */ export const FORM_UNAVAILABLE_MESSAGE = 'This form isn’t accepting messages right now, so your message was not ' + 'sent. Please try again later.';
55
+ /**
56
+ * A site's published support address is author-written, so it is validated
57
+ * before being rendered as a link rather than trusted. Conservative on
58
+ * purpose: a rejected address costs the visitor a convenience, an accepted
59
+ * junk one costs them a bounced email and the site a lead it never learns it
60
+ * lost. The `mailto:` scheme is written by the renderer, not taken from here,
61
+ * so this is a plausibility check and not the injection defence.
62
+ */ const CONTACT_EMAIL_PATTERN = /^[^\s@<>"']+@[^\s@<>"'.]+(\.[^\s@<>"'.]+)+$/;
63
+ /**
64
+ * Does this refused submit mean "this site's form is not accepting", rather
65
+ * than "something broke"?
66
+ *
67
+ * Takes the BODY only, deliberately. The obvious signature — `(status, body)`,
68
+ * matching `parseLockdownRefusal` — invites a caller to gate on the status
69
+ * first, and the status is precisely what does not distinguish this refusal
70
+ * from the Free plan's monthly wall. A caller holding only this function
71
+ * cannot make that mistake.
72
+ *
73
+ * Returns `null` for every other body, so a caller's existing generic error
74
+ * branch keeps handling real failures. Dressing a 500 as a deliberate pause
75
+ * would be the same class of lie in the other direction.
76
+ */ export function parseFormUnavailableRefusal(body) {
77
+ if (!body || typeof body !== 'object') return null;
78
+ const payload = body;
79
+ if (payload.code !== FORM_ABUSE_CEILING_CODE) return null;
80
+ const contact = typeof payload.contact === 'string' && payload.contact.trim() ? payload.contact.trim() : undefined;
81
+ return _extends({
82
+ message: FORM_UNAVAILABLE_MESSAGE
83
+ }, contact && CONTACT_EMAIL_PATTERN.test(contact) ? {
84
+ contact
85
+ } : {});
86
+ }
87
+
88
+ //# sourceMappingURL=form-unavailable.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/forms/src/lib/model/form-unavailable.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * What a VISITOR is told when a site's form is not accepting (AGL-1666).\n *\n * The door answers a flooded site's submissions with a refusal the form\n * element recognises by its code, and the element shows the sentence below.\n * The owner's half of the same moment — the count, the ceiling and when it\n * lifts — is the forms plugin's `visitorDoors` declaration, which the Inbox\n * and the staff pages read without loading this plugin. The two halves are\n * constrained in opposite directions: the visitor is a stranger and must not\n * be told why; the owner needs exactly that.\n */\n\n/**\n * Refusal code carried by the abuse-ceiling 429 (AGL-1655).\n *\n * The Free plan's monthly wall answers 429 too, so the STATUS identifies\n * nothing — this code is the whole discriminator, and one means \"the customer\n * needs a bigger plan\" while the other means \"this site is being flooded\".\n * Shared rather than restated so the producer and the consumer cannot drift.\n */\nexport const FORM_ABUSE_CEILING_CODE = 'form-abuse-ceiling'\n\n/** What the visitor is shown when a site's form is not accepting. */\nexport interface FormUnavailableNotice {\n /** The whole message. Fixed copy — never the server's terse `error`. */\n message: string\n /**\n * An alternative way to reach the site, when the site publishes one\n * (`business.supportEmail`, the token whose own description is \"where\n * visitors should write for help\"). Absent when it does not.\n */\n contact?: string\n}\n\n/**\n * The visitor's sentence.\n *\n * Three things it must not do, all of which the generic error branch did or\n * a friendlier rewrite would:\n *\n * 1. **Not blame the visitor.** They filled in a form correctly. Nothing\n * about their message, their address or their retry is the reason.\n * 2. **Not explain.** \"Unusual volume\", \"temporarily over its limit\", even\n * \"this site is receiving too many messages\" all tell a stranger\n * something about the site owner's account that the owner never chose to\n * publish. The refusal is the owner's problem to see, not the caller's.\n * 3. **Not sound delivered.** \"We'll get back to you\", or anything ending\n * in a thank-you, converts a lost lead into a person waiting for a reply\n * that is never coming. So it says, in as many words, that the message\n * was not sent.\n *\n * What is left is short on purpose. The one genuinely useful thing to add is\n * a door that still opens, which is why `contact` rides along.\n */\nexport const FORM_UNAVAILABLE_MESSAGE =\n 'This form isn’t accepting messages right now, so your message was not ' +\n 'sent. Please try again later.'\n\n/**\n * A site's published support address is author-written, so it is validated\n * before being rendered as a link rather than trusted. Conservative on\n * purpose: a rejected address costs the visitor a convenience, an accepted\n * junk one costs them a bounced email and the site a lead it never learns it\n * lost. The `mailto:` scheme is written by the renderer, not taken from here,\n * so this is a plausibility check and not the injection defence.\n */\nconst CONTACT_EMAIL_PATTERN = /^[^\\s@<>\"']+@[^\\s@<>\"'.]+(\\.[^\\s@<>\"'.]+)+$/\n\n/**\n * Does this refused submit mean \"this site's form is not accepting\", rather\n * than \"something broke\"?\n *\n * Takes the BODY only, deliberately. The obvious signature — `(status, body)`,\n * matching `parseLockdownRefusal` — invites a caller to gate on the status\n * first, and the status is precisely what does not distinguish this refusal\n * from the Free plan's monthly wall. A caller holding only this function\n * cannot make that mistake.\n *\n * Returns `null` for every other body, so a caller's existing generic error\n * branch keeps handling real failures. Dressing a 500 as a deliberate pause\n * would be the same class of lie in the other direction.\n */\nexport function parseFormUnavailableRefusal(\n body: unknown,\n): FormUnavailableNotice | null {\n if (!body || typeof body !== 'object') return null\n const payload = body as { code?: unknown; contact?: unknown }\n if (payload.code !== FORM_ABUSE_CEILING_CODE) return null\n const contact =\n typeof payload.contact === 'string' && payload.contact.trim()\n ? payload.contact.trim()\n : undefined\n return {\n message: FORM_UNAVAILABLE_MESSAGE,\n ...(contact && CONTACT_EMAIL_PATTERN.test(contact) ? { contact } : {}),\n }\n}\n"],"names":["FORM_ABUSE_CEILING_CODE","FORM_UNAVAILABLE_MESSAGE","CONTACT_EMAIL_PATTERN","parseFormUnavailableRefusal","body","payload","code","contact","trim","undefined","message","test"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;CAUC,GAED;;;;;;;CAOC,GACD,OAAO,MAAMA,0BAA0B,qBAAoB;AAc3D;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,MAAMC,2BACX,2EACA,gCAA+B;AAEjC;;;;;;;CAOC,GACD,MAAMC,wBAAwB;AAE9B;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASC,4BACdC,IAAa;IAEb,IAAI,CAACA,QAAQ,OAAOA,SAAS,UAAU,OAAO;IAC9C,MAAMC,UAAUD;IAChB,IAAIC,QAAQC,IAAI,KAAKN,yBAAyB,OAAO;IACrD,MAAMO,UACJ,OAAOF,QAAQE,OAAO,KAAK,YAAYF,QAAQE,OAAO,CAACC,IAAI,KACvDH,QAAQE,OAAO,CAACC,IAAI,KACpBC;IACN,OAAO;QACLC,SAAST;OACLM,WAAWL,sBAAsBS,IAAI,CAACJ,WAAW;QAAEA;IAAQ,IAAI,CAAC;AAExE"}