@aglyn/plugins-forms 1.0.0-beta.221 → 1.0.0-beta.223
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 +1 -0
- package/package.json +14 -12
- package/src/lib/components/form-detail-card.js +166 -29
- package/src/lib/components/form-detail-card.js.map +1 -1
- package/src/lib/components/form-submissions-card.component.d.ts +12 -0
- package/src/lib/components/form-submissions-card.component.js +35 -2
- package/src/lib/components/form-submissions-card.component.js.map +1 -1
- package/src/lib/components/form.js +2 -1
- package/src/lib/components/form.js.map +1 -1
- package/src/lib/components/host-forms-card.component.d.ts +8 -0
- package/src/lib/components/host-forms-card.component.js +26 -1
- package/src/lib/components/host-forms-card.component.js.map +1 -1
- package/src/lib/components/site-package-form-preview.component.d.ts +13 -0
- package/src/lib/components/site-package-form-preview.component.js +57 -0
- package/src/lib/components/site-package-form-preview.component.js.map +1 -0
- package/src/lib/constants/site-package.d.ts +18 -0
- package/src/lib/constants/site-package.js +18 -0
- package/src/lib/constants/site-package.js.map +1 -0
- package/src/lib/declarations.console-server.d.ts +39 -0
- package/src/lib/declarations.console-server.js +68 -0
- package/src/lib/declarations.console-server.js.map +1 -0
- package/src/lib/declarations.server.js +22 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/model/form-door.d.ts +37 -0
- package/src/lib/model/form-door.js +50 -0
- package/src/lib/model/form-door.js.map +1 -0
- package/src/lib/model/form-submission-counts.d.ts +26 -0
- package/src/lib/model/form-submission-counts.js +35 -0
- package/src/lib/model/form-submission-counts.js.map +1 -0
- package/src/lib/model/form-submission-list.d.ts +40 -0
- package/src/lib/model/form-submission-list.js +66 -0
- package/src/lib/model/form-submission-list.js.map +1 -0
- package/src/lib/model/form-submission-record.d.ts +52 -0
- package/src/lib/model/form-submission-record.js +70 -0
- package/src/lib/model/form-submission-record.js.map +1 -0
- package/src/lib/model/form-unavailable.d.ts +83 -0
- package/src/lib/model/form-unavailable.js +88 -0
- package/src/lib/model/form-unavailable.js.map +1 -0
- package/src/lib/model/forms-record-lists.d.ts +31 -0
- package/src/lib/model/forms-record-lists.js +55 -0
- package/src/lib/model/forms-record-lists.js.map +1 -0
- package/src/lib/plan-entitlements.js +6 -6
- package/src/lib/plan-entitlements.js.map +1 -1
- package/src/lib/plugin.js +34 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/api-v1/form-submissions.d.ts +34 -0
- package/src/lib/server/api-v1/form-submissions.js +273 -0
- package/src/lib/server/api-v1/form-submissions.js.map +1 -0
- package/src/lib/server/api-v1/openapi.d.ts +26 -0
- package/src/lib/server/api-v1/openapi.js +133 -0
- package/src/lib/server/api-v1/openapi.js.map +1 -0
- package/src/lib/server/form-intake-gate.d.ts +34 -0
- package/src/lib/server/form-intake-gate.js +44 -0
- package/src/lib/server/form-intake-gate.js.map +1 -0
- package/src/lib/server/form-submission-index.d.ts +26 -0
- package/src/lib/server/form-submission-index.js +51 -0
- package/src/lib/server/form-submission-index.js.map +1 -0
- package/src/lib/server/form-submit.d.ts +43 -0
- package/src/lib/server/form-submit.js +850 -0
- package/src/lib/server/form-submit.js.map +1 -0
- package/src/lib/server/increment-form-stats.d.ts +70 -0
- package/src/lib/server/increment-form-stats.js +112 -0
- package/src/lib/server/increment-form-stats.js.map +1 -0
- package/src/lib/server.d.ts +10 -0
- package/src/lib/server.js +15 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/transfer/form-submissions-transfer-key.d.ts +37 -0
- package/src/lib/transfer/form-submissions-transfer-key.js +40 -0
- package/src/lib/transfer/form-submissions-transfer-key.js.map +1 -0
- package/src/lib/transfer/form-submissions-transfer.d.ts +98 -0
- package/src/lib/transfer/form-submissions-transfer.js +563 -0
- package/src/lib/transfer/form-submissions-transfer.js.map +1 -0
- package/src/lib/transfer/form-submissions-transfer.server.d.ts +26 -0
- package/src/lib/transfer/form-submissions-transfer.server.js +31 -0
- package/src/lib/transfer/form-submissions-transfer.server.js.map +1 -0
|
@@ -0,0 +1,133 @@
|
|
|
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 { booleanField as bool, objectKindField as OBJECT_FIELD, openObjectField as objectOf, queryParam as q, RECORD_STAMPS as STAMPS, stringField as str } from "@aglyn/tenant-data-admin/server/api-v1-description";
|
|
17
|
+
/**
|
|
18
|
+
* `/v1/sites/{siteId}/form-submissions` as the customer API's OpenAPI
|
|
19
|
+
* document describes it — what the console's builder turns into paths, a tag
|
|
20
|
+
* and schemas, and the MCP tools derive from. Transcribed unchanged from the
|
|
21
|
+
* builder's own list when the resource moved here (AGL-3080);
|
|
22
|
+
* `api-v1-openapi.spec.ts` holds the document to every endpoint the
|
|
23
|
+
* documentation promises.
|
|
24
|
+
*/ export const FORM_SUBMISSIONS_API_V1_DESCRIPTION = {
|
|
25
|
+
tag: 'Form submissions',
|
|
26
|
+
description: 'What visitors sent through a site’s forms.',
|
|
27
|
+
schemaName: 'FormSubmission',
|
|
28
|
+
required: [
|
|
29
|
+
'id',
|
|
30
|
+
'object',
|
|
31
|
+
'form_id',
|
|
32
|
+
'fields'
|
|
33
|
+
],
|
|
34
|
+
writable: [
|
|
35
|
+
'read'
|
|
36
|
+
],
|
|
37
|
+
writeRequired: [
|
|
38
|
+
'read'
|
|
39
|
+
],
|
|
40
|
+
fields: {
|
|
41
|
+
id: str('Submission id.'),
|
|
42
|
+
object: OBJECT_FIELD('form_submission'),
|
|
43
|
+
form_id: str('Form id.'),
|
|
44
|
+
form: str('Form name.'),
|
|
45
|
+
path: str('Page path the form was submitted from.'),
|
|
46
|
+
fields: objectOf('The submitted values, keyed by field name.'),
|
|
47
|
+
read: bool('Whether the submission has been marked read.'),
|
|
48
|
+
routing: objectOf('Where the submission was routed.'),
|
|
49
|
+
created: STAMPS.created
|
|
50
|
+
},
|
|
51
|
+
ops: [
|
|
52
|
+
{
|
|
53
|
+
path: '/v1/sites/{siteId}/form-submissions',
|
|
54
|
+
method: 'get',
|
|
55
|
+
operationId: 'listFormSubmissions',
|
|
56
|
+
summary: 'List submissions',
|
|
57
|
+
list: true,
|
|
58
|
+
returns: 'FormSubmission',
|
|
59
|
+
description: 'Combining `form`/`formId` with `read` narrows on the form and checks `read` on the page, so pages can come back short.',
|
|
60
|
+
filters: [
|
|
61
|
+
q('form', 'Form name.'),
|
|
62
|
+
q('formId', 'Form id.'),
|
|
63
|
+
q('read', 'Either `true` or `false`. Anything else is a 400.', {
|
|
64
|
+
type: 'string',
|
|
65
|
+
enum: [
|
|
66
|
+
'true',
|
|
67
|
+
'false'
|
|
68
|
+
]
|
|
69
|
+
})
|
|
70
|
+
],
|
|
71
|
+
pathParams: [
|
|
72
|
+
{
|
|
73
|
+
name: 'siteId',
|
|
74
|
+
description: 'Site id.'
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
path: '/v1/sites/{siteId}/form-submissions/{submissionId}',
|
|
80
|
+
method: 'get',
|
|
81
|
+
operationId: 'getFormSubmission',
|
|
82
|
+
summary: 'Retrieve a submission',
|
|
83
|
+
returns: 'FormSubmission',
|
|
84
|
+
pathParams: [
|
|
85
|
+
{
|
|
86
|
+
name: 'siteId',
|
|
87
|
+
description: 'Site id.'
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: 'submissionId',
|
|
91
|
+
description: 'Submission id.'
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
path: '/v1/sites/{siteId}/form-submissions/{submissionId}',
|
|
97
|
+
method: 'patch',
|
|
98
|
+
operationId: 'updateFormSubmission',
|
|
99
|
+
summary: 'Mark a submission read',
|
|
100
|
+
accepts: 'FormSubmissionWrite',
|
|
101
|
+
returns: 'FormSubmission',
|
|
102
|
+
pathParams: [
|
|
103
|
+
{
|
|
104
|
+
name: 'siteId',
|
|
105
|
+
description: 'Site id.'
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: 'submissionId',
|
|
109
|
+
description: 'Submission id.'
|
|
110
|
+
}
|
|
111
|
+
]
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
path: '/v1/sites/{siteId}/form-submissions/{submissionId}',
|
|
115
|
+
method: 'delete',
|
|
116
|
+
operationId: 'deleteFormSubmission',
|
|
117
|
+
summary: 'Delete a submission',
|
|
118
|
+
returns: 'Deleted',
|
|
119
|
+
pathParams: [
|
|
120
|
+
{
|
|
121
|
+
name: 'siteId',
|
|
122
|
+
description: 'Site id.'
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: 'submissionId',
|
|
126
|
+
description: 'Submission id.'
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
]
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
//# sourceMappingURL=openapi.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/plugins/forms/src/lib/server/api-v1/openapi.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 type ApiV1ResourceDescription,\n booleanField as bool,\n objectKindField as OBJECT_FIELD,\n openObjectField as objectOf,\n queryParam as q,\n RECORD_STAMPS as STAMPS,\n stringField as str,\n} from '@aglyn/tenant-data-admin/server/api-v1-description'\n\n/**\n * `/v1/sites/{siteId}/form-submissions` as the customer API's OpenAPI\n * document describes it — what the console's builder turns into paths, a tag\n * and schemas, and the MCP tools derive from. Transcribed unchanged from the\n * builder's own list when the resource moved here (AGL-3080);\n * `api-v1-openapi.spec.ts` holds the document to every endpoint the\n * documentation promises.\n */\nexport const FORM_SUBMISSIONS_API_V1_DESCRIPTION: ApiV1ResourceDescription = {\n tag: 'Form submissions',\n description: 'What visitors sent through a site’s forms.',\n schemaName: 'FormSubmission',\n required: ['id', 'object', 'form_id', 'fields'],\n writable: ['read'],\n writeRequired: ['read'],\n fields: {\n id: str('Submission id.'),\n object: OBJECT_FIELD('form_submission'),\n form_id: str('Form id.'),\n form: str('Form name.'),\n path: str('Page path the form was submitted from.'),\n fields: objectOf('The submitted values, keyed by field name.'),\n read: bool('Whether the submission has been marked read.'),\n routing: objectOf('Where the submission was routed.'),\n created: STAMPS.created,\n },\n ops: [\n { path: '/v1/sites/{siteId}/form-submissions', method: 'get', operationId: 'listFormSubmissions', summary: 'List submissions', list: true, returns: 'FormSubmission', description: 'Combining `form`/`formId` with `read` narrows on the form and checks `read` on the page, so pages can come back short.', filters: [q('form', 'Form name.'), q('formId', 'Form id.'), q('read', 'Either `true` or `false`. Anything else is a 400.', { type: 'string', enum: ['true', 'false'] })], pathParams: [{ name: 'siteId', description: 'Site id.' }] },\n { path: '/v1/sites/{siteId}/form-submissions/{submissionId}', method: 'get', operationId: 'getFormSubmission', summary: 'Retrieve a submission', returns: 'FormSubmission', pathParams: [{ name: 'siteId', description: 'Site id.' }, { name: 'submissionId', description: 'Submission id.' }] },\n { path: '/v1/sites/{siteId}/form-submissions/{submissionId}', method: 'patch', operationId: 'updateFormSubmission', summary: 'Mark a submission read', accepts: 'FormSubmissionWrite', returns: 'FormSubmission', pathParams: [{ name: 'siteId', description: 'Site id.' }, { name: 'submissionId', description: 'Submission id.' }] },\n { path: '/v1/sites/{siteId}/form-submissions/{submissionId}', method: 'delete', operationId: 'deleteFormSubmission', summary: 'Delete a submission', returns: 'Deleted', pathParams: [{ name: 'siteId', description: 'Site id.' }, { name: 'submissionId', description: 'Submission id.' }] },\n ],\n}\n"],"names":["booleanField","bool","objectKindField","OBJECT_FIELD","openObjectField","objectOf","queryParam","q","RECORD_STAMPS","STAMPS","stringField","str","FORM_SUBMISSIONS_API_V1_DESCRIPTION","tag","description","schemaName","required","writable","writeRequired","fields","id","object","form_id","form","path","read","routing","created","ops","method","operationId","summary","list","returns","filters","type","enum","pathParams","name","accepts"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAEEA,gBAAgBC,IAAI,EACpBC,mBAAmBC,YAAY,EAC/BC,mBAAmBC,QAAQ,EAC3BC,cAAcC,CAAC,EACfC,iBAAiBC,MAAM,EACvBC,eAAeC,GAAG,QACb,qDAAoD;AAE3D;;;;;;;CAOC,GACD,OAAO,MAAMC,sCAAgE;IAC3EC,KAAK;IACLC,aAAa;IACbC,YAAY;IACZC,UAAU;QAAC;QAAM;QAAU;QAAW;KAAS;IAC/CC,UAAU;QAAC;KAAO;IAClBC,eAAe;QAAC;KAAO;IACvBC,QAAQ;QACNC,IAAIT,IAAI;QACRU,QAAQlB,aAAa;QACrBmB,SAASX,IAAI;QACbY,MAAMZ,IAAI;QACVa,MAAMb,IAAI;QACVQ,QAAQd,SAAS;QACjBoB,MAAMxB,KAAK;QACXyB,SAASrB,SAAS;QAClBsB,SAASlB,OAAOkB,OAAO;IACzB;IACAC,KAAK;QACH;YAAEJ,MAAM;YAAuCK,QAAQ;YAAOC,aAAa;YAAuBC,SAAS;YAAoBC,MAAM;YAAMC,SAAS;YAAkBnB,aAAa;YAA0HoB,SAAS;gBAAC3B,EAAE,QAAQ;gBAAeA,EAAE,UAAU;gBAAaA,EAAE,QAAQ,qDAAqD;oBAAE4B,MAAM;oBAAUC,MAAM;wBAAC;wBAAQ;qBAAQ;gBAAC;aAAG;YAAEC,YAAY;gBAAC;oBAAEC,MAAM;oBAAUxB,aAAa;gBAAW;aAAE;QAAC;QACjhB;YAAEU,MAAM;YAAsDK,QAAQ;YAAOC,aAAa;YAAqBC,SAAS;YAAyBE,SAAS;YAAkBI,YAAY;gBAAC;oBAAEC,MAAM;oBAAUxB,aAAa;gBAAW;gBAAG;oBAAEwB,MAAM;oBAAgBxB,aAAa;gBAAiB;aAAE;QAAC;QAC/R;YAAEU,MAAM;YAAsDK,QAAQ;YAASC,aAAa;YAAwBC,SAAS;YAA0BQ,SAAS;YAAuBN,SAAS;YAAkBI,YAAY;gBAAC;oBAAEC,MAAM;oBAAUxB,aAAa;gBAAW;gBAAG;oBAAEwB,MAAM;oBAAgBxB,aAAa;gBAAiB;aAAE;QAAC;QACrU;YAAEU,MAAM;YAAsDK,QAAQ;YAAUC,aAAa;YAAwBC,SAAS;YAAuBE,SAAS;YAAWI,YAAY;gBAAC;oBAAEC,MAAM;oBAAUxB,aAAa;gBAAW;gBAAG;oBAAEwB,MAAM;oBAAgBxB,aAAa;gBAAiB;aAAE;QAAC;KAC7R;AACH,EAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
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 { PluginIntakeGate } from '@aglyn/aglyn/plugin-manager/plugin-intake-gates';
|
|
18
|
+
/**
|
|
19
|
+
* The form door, asked without writing (AGL-2586, AGL-3080).
|
|
20
|
+
*
|
|
21
|
+
* The gates `POST /api/forms/submit` clears before its first write that are
|
|
22
|
+
* this plugin's, in the order it clears them, through the same functions: the
|
|
23
|
+
* site's Forms switch (AGL-3029), the plan's monthly allowance (a wall on
|
|
24
|
+
* Free, a meter on the plans that carry the infra pass-through, AGL-1280) and
|
|
25
|
+
* the ceiling that contains a flood (AGL-1655), the last two over the month's
|
|
26
|
+
* count the route reads. The funnel probe on `/api/health/funnel` asks this
|
|
27
|
+
* through `plugin-intake-gates`, so its verdict is the route's predicates
|
|
28
|
+
* about the real documents, and it writes nothing.
|
|
29
|
+
*
|
|
30
|
+
* The door is judged by this plugin's switch alone: the probe asks about a
|
|
31
|
+
* form, and a Marketing popup's capture through the same door is the route's
|
|
32
|
+
* to judge by its own plugin.
|
|
33
|
+
*/
|
|
34
|
+
export declare const formIntakeGate: PluginIntakeGate;
|
|
@@ -0,0 +1,44 @@
|
|
|
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 { checkFormSubmissionAbuseCeiling, checkFormSubmissionQuota, isHostPluginEnabled, utcMonthKey } from "@aglyn/aglyn/server";
|
|
17
|
+
import { firebaseAdmin } from "@aglyn/tenant-data-admin/server/firebase-admin";
|
|
18
|
+
import { BUNDLE_ID } from "../constants/bundle-common.js";
|
|
19
|
+
/**
|
|
20
|
+
* The form door, asked without writing (AGL-2586, AGL-3080).
|
|
21
|
+
*
|
|
22
|
+
* The gates `POST /api/forms/submit` clears before its first write that are
|
|
23
|
+
* this plugin's, in the order it clears them, through the same functions: the
|
|
24
|
+
* site's Forms switch (AGL-3029), the plan's monthly allowance (a wall on
|
|
25
|
+
* Free, a meter on the plans that carry the infra pass-through, AGL-1280) and
|
|
26
|
+
* the ceiling that contains a flood (AGL-1655), the last two over the month's
|
|
27
|
+
* count the route reads. The funnel probe on `/api/health/funnel` asks this
|
|
28
|
+
* through `plugin-intake-gates`, so its verdict is the route's predicates
|
|
29
|
+
* about the real documents, and it writes nothing.
|
|
30
|
+
*
|
|
31
|
+
* The door is judged by this plugin's switch alone: the probe asks about a
|
|
32
|
+
* form, and a Marketing popup's capture through the same door is the route's
|
|
33
|
+
* to judge by its own plugin.
|
|
34
|
+
*/ export const formIntakeGate = async ({ hostId, host, org })=>{
|
|
35
|
+
var _counter_get;
|
|
36
|
+
if (!isHostPluginEnabled(org, host, BUNDLE_ID)) return 'switched-off';
|
|
37
|
+
const counter = await firebaseAdmin.app().firestore().collection('hosts').doc(hostId).collection('counters').doc('formSubmissions').get();
|
|
38
|
+
const used = Number((_counter_get = counter.get(utcMonthKey())) != null ? _counter_get : 0);
|
|
39
|
+
if (!checkFormSubmissionQuota(org, used).allowed) return 'plan-exhausted';
|
|
40
|
+
if (checkFormSubmissionAbuseCeiling(org, used).exceeded) return 'flood-ceiling';
|
|
41
|
+
return 'open';
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
//# sourceMappingURL=form-intake-gate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/forms/src/lib/server/form-intake-gate.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 checkFormSubmissionAbuseCeiling,\n checkFormSubmissionQuota,\n isHostPluginEnabled,\n utcMonthKey,\n} from '@aglyn/aglyn/server'\nimport type { PluginIntakeGate } from '@aglyn/aglyn/plugin-manager/plugin-intake-gates'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { BUNDLE_ID } from '../constants/bundle-common'\n\n/**\n * The form door, asked without writing (AGL-2586, AGL-3080).\n *\n * The gates `POST /api/forms/submit` clears before its first write that are\n * this plugin's, in the order it clears them, through the same functions: the\n * site's Forms switch (AGL-3029), the plan's monthly allowance (a wall on\n * Free, a meter on the plans that carry the infra pass-through, AGL-1280) and\n * the ceiling that contains a flood (AGL-1655), the last two over the month's\n * count the route reads. The funnel probe on `/api/health/funnel` asks this\n * through `plugin-intake-gates`, so its verdict is the route's predicates\n * about the real documents, and it writes nothing.\n *\n * The door is judged by this plugin's switch alone: the probe asks about a\n * form, and a Marketing popup's capture through the same door is the route's\n * to judge by its own plugin.\n */\nexport const formIntakeGate: PluginIntakeGate = async ({ hostId, host, org }) => {\n if (!isHostPluginEnabled(org as never, host as never, BUNDLE_ID)) return 'switched-off'\n const counter = await firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(hostId)\n .collection('counters')\n .doc('formSubmissions')\n .get()\n const used = Number(counter.get(utcMonthKey()) ?? 0)\n if (!checkFormSubmissionQuota(org as never, used).allowed) return 'plan-exhausted'\n if (checkFormSubmissionAbuseCeiling(org as never, used).exceeded) return 'flood-ceiling'\n return 'open'\n}\n"],"names":["checkFormSubmissionAbuseCeiling","checkFormSubmissionQuota","isHostPluginEnabled","utcMonthKey","firebaseAdmin","BUNDLE_ID","formIntakeGate","hostId","host","org","counter","app","firestore","collection","doc","get","used","Number","allowed","exceeded"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,+BAA+B,EAC/BC,wBAAwB,EACxBC,mBAAmB,EACnBC,WAAW,QACN,sBAAqB;AAE5B,SAASC,aAAa,QAAQ,iDAAgD;AAC9E,SAASC,SAAS,QAAQ,gCAA4B;AAEtD;;;;;;;;;;;;;;;CAeC,GACD,OAAO,MAAMC,iBAAmC,OAAO,EAAEC,MAAM,EAAEC,IAAI,EAAEC,GAAG,EAAE;QAUtDC;IATpB,IAAI,CAACR,oBAAoBO,KAAcD,MAAeH,YAAY,OAAO;IACzE,MAAMK,UAAU,MAAMN,cACnBO,GAAG,GACHC,SAAS,GACTC,UAAU,CAAC,SACXC,GAAG,CAACP,QACJM,UAAU,CAAC,YACXC,GAAG,CAAC,mBACJC,GAAG;IACN,MAAMC,OAAOC,QAAOP,eAAAA,QAAQK,GAAG,CAACZ,0BAAZO,eAA8B;IAClD,IAAI,CAACT,yBAAyBQ,KAAcO,MAAME,OAAO,EAAE,OAAO;IAClE,IAAIlB,gCAAgCS,KAAcO,MAAMG,QAAQ,EAAE,OAAO;IACzE,OAAO;AACT,EAAC"}
|
|
@@ -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 { PluginRecordIndex } from '@aglyn/aglyn/plugin-manager/plugin-record-index';
|
|
18
|
+
/**
|
|
19
|
+
* A site's form submissions on the server, for the plugin that reads them
|
|
20
|
+
* (AGL-3080) — the Inbox's reply and its list assignment. Site-scoped: a
|
|
21
|
+
* request that names no site answers none. Each record is
|
|
22
|
+
* `formSubmissionIndexedRecord`'s; `ref` hands the reader the one document it
|
|
23
|
+
* marks answered and threads its own records under, as that module documents.
|
|
24
|
+
* The reader proves who is asking first — the index authenticates nobody.
|
|
25
|
+
*/
|
|
26
|
+
export declare const formSubmissionRecordIndex: PluginRecordIndex;
|
|
@@ -0,0 +1,51 @@
|
|
|
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 { firebaseAdmin } from "@aglyn/tenant-data-admin/server/firebase-admin";
|
|
17
|
+
import { formSubmissionIndexedRecord } from "../model/form-submission-record.js";
|
|
18
|
+
/** The collection this plugin keeps submissions in, under a site. */ const SUBMISSIONS = 'formSubmissions';
|
|
19
|
+
const submissions = (hostId)=>firebaseAdmin.app().firestore().collection('hosts').doc(hostId).collection(SUBMISSIONS);
|
|
20
|
+
/**
|
|
21
|
+
* A site's form submissions on the server, for the plugin that reads them
|
|
22
|
+
* (AGL-3080) — the Inbox's reply and its list assignment. Site-scoped: a
|
|
23
|
+
* request that names no site answers none. Each record is
|
|
24
|
+
* `formSubmissionIndexedRecord`'s; `ref` hands the reader the one document it
|
|
25
|
+
* marks answered and threads its own records under, as that module documents.
|
|
26
|
+
* The reader proves who is asking first — the index authenticates nobody.
|
|
27
|
+
*/ export const formSubmissionRecordIndex = {
|
|
28
|
+
async list ({ hostId, limit }) {
|
|
29
|
+
if (!hostId || limit < 1) return {
|
|
30
|
+
records: [],
|
|
31
|
+
truncated: false
|
|
32
|
+
};
|
|
33
|
+
const snapshot = await submissions(hostId).orderBy('createdAt', 'desc').limit(limit + 1).get();
|
|
34
|
+
const records = snapshot.docs.slice(0, limit).map((document)=>formSubmissionIndexedRecord(document.id, document.data())).filter((record)=>record !== null);
|
|
35
|
+
return {
|
|
36
|
+
records,
|
|
37
|
+
truncated: snapshot.size > limit
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
async get ({ hostId, id }) {
|
|
41
|
+
if (!hostId || !id) return null;
|
|
42
|
+
const snapshot = await submissions(hostId).doc(id).get();
|
|
43
|
+
return snapshot.exists ? formSubmissionIndexedRecord(snapshot.id, snapshot.data()) : null;
|
|
44
|
+
},
|
|
45
|
+
async ref ({ hostId, id }) {
|
|
46
|
+
if (!hostId || !id) return null;
|
|
47
|
+
return submissions(hostId).doc(id);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
//# sourceMappingURL=form-submission-index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/forms/src/lib/server/form-submission-index.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 { PluginRecordIndex } from '@aglyn/aglyn/plugin-manager/plugin-record-index'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { formSubmissionIndexedRecord } from '../model/form-submission-record'\n\n/** The collection this plugin keeps submissions in, under a site. */\nconst SUBMISSIONS = 'formSubmissions'\n\nconst submissions = (hostId: string) =>\n firebaseAdmin.app().firestore().collection('hosts').doc(hostId).collection(SUBMISSIONS)\n\n/**\n * A site's form submissions on the server, for the plugin that reads them\n * (AGL-3080) — the Inbox's reply and its list assignment. Site-scoped: a\n * request that names no site answers none. Each record is\n * `formSubmissionIndexedRecord`'s; `ref` hands the reader the one document it\n * marks answered and threads its own records under, as that module documents.\n * The reader proves who is asking first — the index authenticates nobody.\n */\nexport const formSubmissionRecordIndex: PluginRecordIndex = {\n async list({ hostId, limit }) {\n if (!hostId || limit < 1) return { records: [], truncated: false }\n const snapshot = await submissions(hostId).orderBy('createdAt', 'desc').limit(limit + 1).get()\n const records = snapshot.docs\n .slice(0, limit)\n .map((document) => formSubmissionIndexedRecord(document.id, document.data()))\n .filter((record): record is NonNullable<typeof record> => record !== null)\n return { records, truncated: snapshot.size > limit }\n },\n async get({ hostId, id }) {\n if (!hostId || !id) return null\n const snapshot = await submissions(hostId).doc(id).get()\n return snapshot.exists ? formSubmissionIndexedRecord(snapshot.id, snapshot.data()) : null\n },\n async ref({ hostId, id }) {\n if (!hostId || !id) return null\n return submissions(hostId).doc(id)\n },\n}\n"],"names":["firebaseAdmin","formSubmissionIndexedRecord","SUBMISSIONS","submissions","hostId","app","firestore","collection","doc","formSubmissionRecordIndex","list","limit","records","truncated","snapshot","orderBy","get","docs","slice","map","document","id","data","filter","record","size","exists","ref"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAGD,SAASA,aAAa,QAAQ,iDAAgD;AAC9E,SAASC,2BAA2B,QAAQ,qCAAiC;AAE7E,mEAAmE,GACnE,MAAMC,cAAc;AAEpB,MAAMC,cAAc,CAACC,SACnBJ,cAAcK,GAAG,GAAGC,SAAS,GAAGC,UAAU,CAAC,SAASC,GAAG,CAACJ,QAAQG,UAAU,CAACL;AAE7E;;;;;;;CAOC,GACD,OAAO,MAAMO,4BAA+C;IAC1D,MAAMC,MAAK,EAAEN,MAAM,EAAEO,KAAK,EAAE;QAC1B,IAAI,CAACP,UAAUO,QAAQ,GAAG,OAAO;YAAEC,SAAS,EAAE;YAAEC,WAAW;QAAM;QACjE,MAAMC,WAAW,MAAMX,YAAYC,QAAQW,OAAO,CAAC,aAAa,QAAQJ,KAAK,CAACA,QAAQ,GAAGK,GAAG;QAC5F,MAAMJ,UAAUE,SAASG,IAAI,CAC1BC,KAAK,CAAC,GAAGP,OACTQ,GAAG,CAAC,CAACC,WAAanB,4BAA4BmB,SAASC,EAAE,EAAED,SAASE,IAAI,KACxEC,MAAM,CAAC,CAACC,SAAiDA,WAAW;QACvE,OAAO;YAAEZ;YAASC,WAAWC,SAASW,IAAI,GAAGd;QAAM;IACrD;IACA,MAAMK,KAAI,EAAEZ,MAAM,EAAEiB,EAAE,EAAE;QACtB,IAAI,CAACjB,UAAU,CAACiB,IAAI,OAAO;QAC3B,MAAMP,WAAW,MAAMX,YAAYC,QAAQI,GAAG,CAACa,IAAIL,GAAG;QACtD,OAAOF,SAASY,MAAM,GAAGzB,4BAA4Ba,SAASO,EAAE,EAAEP,SAASQ,IAAI,MAAM;IACvF;IACA,MAAMK,KAAI,EAAEvB,MAAM,EAAEiB,EAAE,EAAE;QACtB,IAAI,CAACjB,UAAU,CAACiB,IAAI,OAAO;QAC3B,OAAOlB,YAAYC,QAAQI,GAAG,CAACa;IACjC;AACF,EAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
* `POST /api/forms/submit` — every form's door, and a Marketing popup's
|
|
19
|
+
* (AGL-76, AGL-3080).
|
|
20
|
+
*
|
|
21
|
+
* This plugin's route, served by the tenant's plugin API dispatcher at the
|
|
22
|
+
* address it has always had, and registered as a route that KEEPS ITS OWN
|
|
23
|
+
* VISITOR GATES (`ownVisitorGates`): the dispatcher hands it the request
|
|
24
|
+
* untouched, because every gate below is a published answer — which plugin's
|
|
25
|
+
* switch decides a submission (the door it came through, not this route's
|
|
26
|
+
* owner), the "not accepting" notice a site with Forms off gives, the
|
|
27
|
+
* per-(site, address) limit and the monthly ceiling, and where lockdown sits
|
|
28
|
+
* among them. The dispatcher's own versions of those would answer first and
|
|
29
|
+
* differently.
|
|
30
|
+
*
|
|
31
|
+
* Lead-capture submissions endpoint (AGL-76): validates the target host,
|
|
32
|
+
* drops honeypot hits silently, applies the plan's monthly submission quota
|
|
33
|
+
* via a per-month counter — a hard wall on free, a meter on plans that carry
|
|
34
|
+
* the infra pass-through (AGL-1280) — refuses above the per-site abuse
|
|
35
|
+
* ceiling (AGL-1655), and stores the submission for the console inbox. Writes
|
|
36
|
+
* go through the admin SDK, so client rules never allow arbitrary submission
|
|
37
|
+
* writes.
|
|
38
|
+
*
|
|
39
|
+
* The two 429s are different answers and must stay distinguishable: the plan
|
|
40
|
+
* wall means the customer's allowance is spent, the ceiling means the site is
|
|
41
|
+
* being flooded. Only the ceiling refusal carries `code`.
|
|
42
|
+
*/
|
|
43
|
+
export declare function POST(request: Request): Promise<Response>;
|