@aglyn/plugins-forms 1.0.0-beta.218 → 1.0.0-beta.220
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/package.json +14 -14
- package/src/index.d.ts +1 -0
- package/src/index.js.map +1 -1
- package/src/lib/components/form-design-preview.component.d.ts +1 -1
- package/src/lib/components/form-design-preview.component.js +1 -1
- package/src/lib/components/form-design-preview.component.js.map +1 -1
- package/src/lib/components/form-detail-card.d.ts +1 -1
- package/src/lib/components/form-detail-card.js +12 -9
- package/src/lib/components/form-detail-card.js.map +1 -1
- package/src/lib/components/form-zones.d.ts +9 -0
- package/src/lib/components/form-zones.js +1 -0
- package/src/lib/components/form-zones.js.map +1 -1
- package/src/lib/components/form.js +5 -5
- package/src/lib/components/form.js.map +1 -1
- package/src/lib/components/host-forms-card.component.js +12 -9
- package/src/lib/components/host-forms-card.component.js.map +1 -1
- package/src/lib/components/use-form-promote-api.d.ts +1 -1
- package/src/lib/components/use-form-promote-api.js +2 -2
- package/src/lib/components/use-form-promote-api.js.map +1 -1
- package/src/lib/constants/bundle-common.d.ts +8 -0
- package/src/lib/constants/bundle-common.js +7 -0
- package/src/lib/constants/bundle-common.js.map +1 -1
- package/src/lib/model/forms-record-routes.d.ts +22 -0
- package/src/lib/model/forms-record-routes.js +53 -0
- package/src/lib/model/forms-record-routes.js.map +1 -0
- package/src/lib/plan-entitlements.d.ts +41 -0
- package/src/lib/plan-entitlements.js +53 -0
- package/src/lib/plan-entitlements.js.map +1 -0
- package/src/lib/plugin.js +13 -1
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/form-promote-route.d.ts +74 -0
- package/src/lib/server/form-promote-route.js +239 -0
- package/src/lib/server/form-promote-route.js.map +1 -0
- package/src/lib/server/form-promotion.d.ts +82 -0
- package/src/lib/server/form-promotion.js +133 -0
- package/src/lib/server/form-promotion.js.map +1 -0
- package/src/lib/server/form-publish-announce.d.ts +63 -0
- package/src/lib/server/form-publish-announce.js +142 -0
- package/src/lib/server/form-publish-announce.js.map +1 -0
- package/src/lib/server.d.ts +1 -0
- package/src/lib/server.js +6 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/usage-axes.d.ts +34 -0
- package/src/lib/usage-axes.js +68 -0
- package/src/lib/usage-axes.js.map +1 -0
|
@@ -0,0 +1,142 @@
|
|
|
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 { nodesPlaceForm, screenRoutePathToUrl } from "@aglyn/aglyn/server";
|
|
17
|
+
import { dropPluginSiteCache } from "@aglyn/aglyn/plugin-manager/plugin-site-cache";
|
|
18
|
+
import { isLiveUsageCandidate, readUsageSources, screenIdsUsingComponentDeep, screenIdsUsingLayoutDeep } from "@aglyn/tenant-data-admin/server/live-page-usage";
|
|
19
|
+
/**
|
|
20
|
+
* How many documents per collection the placement scan reads — the bound the
|
|
21
|
+
* platform's own publish scans use, so a form publish and a component publish
|
|
22
|
+
* read the same amount of the same site.
|
|
23
|
+
*/ const SCAN_LIMIT = 2000;
|
|
24
|
+
/**
|
|
25
|
+
* Every live screen whose rendered output contains the form `formId`, however
|
|
26
|
+
* indirectly.
|
|
27
|
+
*
|
|
28
|
+
* The form twin of {@link screenIdsUsingComponentDeep}, and the same walk for
|
|
29
|
+
* the same reason: a placed form is found by SEARCHING node trees, not by
|
|
30
|
+
* matching a pointer, and it can sit on a screen, inside page chrome, or
|
|
31
|
+
* inside a reusable component that a third component nests. Publishing a form
|
|
32
|
+
* changes every page that renders it, so anything this misses keeps serving
|
|
33
|
+
* the old fields for the whole revalidate window while the editor reports that
|
|
34
|
+
* the live sites now serve the new design.
|
|
35
|
+
*
|
|
36
|
+
* The first level is the only thing that differs from the component walk: a
|
|
37
|
+
* document uses a form by PLACING it. After that the fan-out is identical, so
|
|
38
|
+
* it is delegated rather than restated — a component that holds the form is
|
|
39
|
+
* reached by whatever reaches that component, and a layout by whatever renders
|
|
40
|
+
* inside it.
|
|
41
|
+
*
|
|
42
|
+
* Pure, and separate from the Firestore read, so the closure is testable
|
|
43
|
+
* without a database. Cycle-safe by delegation: both walks it defers to bound
|
|
44
|
+
* themselves.
|
|
45
|
+
*/ export function screenIdsUsingFormDeep(formId, sources) {
|
|
46
|
+
if (!formId) return [];
|
|
47
|
+
const screenIds = new Set();
|
|
48
|
+
for (const candidate of sources.screens){
|
|
49
|
+
if (!isLiveUsageCandidate(candidate)) continue;
|
|
50
|
+
if (nodesPlaceForm(candidate.nodes, formId)) screenIds.add(candidate.id);
|
|
51
|
+
}
|
|
52
|
+
for (const candidate of sources.layouts){
|
|
53
|
+
if (!isLiveUsageCandidate(candidate)) continue;
|
|
54
|
+
if (!nodesPlaceForm(candidate.nodes, formId)) continue;
|
|
55
|
+
// The layout itself renders no URL; the screens beneath it do.
|
|
56
|
+
for (const screenId of screenIdsUsingLayoutDeep(candidate.id, sources.screens, sources.layouts)){
|
|
57
|
+
screenIds.add(screenId);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
for (const candidate of sources.components){
|
|
61
|
+
if (!isLiveUsageCandidate(candidate)) continue;
|
|
62
|
+
if (!nodesPlaceForm(candidate.nodes, formId)) continue;
|
|
63
|
+
// And a component renders wherever it is placed, however deeply nested.
|
|
64
|
+
for (const screenId of screenIdsUsingComponentDeep(candidate.id, sources)){
|
|
65
|
+
screenIds.add(screenId);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return [
|
|
69
|
+
...screenIds
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Drop the cached pages that place `formId`, after its design was published.
|
|
74
|
+
*
|
|
75
|
+
* The plugin knows WHICH pages — the ones whose node trees place the form,
|
|
76
|
+
* found by {@link screenIdsUsingFormDeep} over the site's screens, layouts and
|
|
77
|
+
* components — and the app knows how to drop one on every address the site
|
|
78
|
+
* answers at, so the addresses go through the platform's site cache
|
|
79
|
+
* (`dropPluginSiteCache`, narrowed by `paths`).
|
|
80
|
+
*
|
|
81
|
+
* BEST EFFORT, ALWAYS. The publish has already succeeded by the time this
|
|
82
|
+
* runs, so a failed cache hint must never make it look failed: every failure
|
|
83
|
+
* resolves, and the render cache's TTL stays underneath as the backstop. A
|
|
84
|
+
* scan the bound truncated drops the whole site instead, because a prefix of
|
|
85
|
+
* the site would report a publish while leaving placed pages stale.
|
|
86
|
+
*/ export async function announceFormPublish(options) {
|
|
87
|
+
const { firestore, hostId, formId } = options;
|
|
88
|
+
const empty = {
|
|
89
|
+
revalidated: 0,
|
|
90
|
+
screenIds: []
|
|
91
|
+
};
|
|
92
|
+
if (!hostId || !formId) return empty;
|
|
93
|
+
try {
|
|
94
|
+
var _hostSnapshot_get;
|
|
95
|
+
const hostRef = firestore.collection('hosts').doc(hostId);
|
|
96
|
+
const [hostSnapshot, sources] = await Promise.all([
|
|
97
|
+
hostRef.get(),
|
|
98
|
+
readUsageSources(hostRef, SCAN_LIMIT)
|
|
99
|
+
]);
|
|
100
|
+
const reason = 'a form was published';
|
|
101
|
+
if (sources.truncated) {
|
|
102
|
+
const result = await dropPluginSiteCache({
|
|
103
|
+
hostIds: [
|
|
104
|
+
hostId
|
|
105
|
+
],
|
|
106
|
+
reason
|
|
107
|
+
});
|
|
108
|
+
return {
|
|
109
|
+
revalidated: result.dropped,
|
|
110
|
+
screenIds: []
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const screenIds = screenIdsUsingFormDeep(formId, sources.candidates);
|
|
114
|
+
// The routing map is `screenId → path`; a screen not in it is not
|
|
115
|
+
// routable, so it has no live page to drop.
|
|
116
|
+
const routes = (_hostSnapshot_get = hostSnapshot.get('screens')) != null ? _hostSnapshot_get : {};
|
|
117
|
+
const paths = screenIds.map((id)=>routes[id]).filter((path)=>Boolean(path)).map((path)=>screenRoutePathToUrl(path));
|
|
118
|
+
if (!paths.length) return {
|
|
119
|
+
revalidated: 0,
|
|
120
|
+
screenIds
|
|
121
|
+
};
|
|
122
|
+
const result = await dropPluginSiteCache({
|
|
123
|
+
hostIds: [
|
|
124
|
+
hostId
|
|
125
|
+
],
|
|
126
|
+
paths: {
|
|
127
|
+
[hostId]: paths
|
|
128
|
+
},
|
|
129
|
+
reason
|
|
130
|
+
});
|
|
131
|
+
return {
|
|
132
|
+
revalidated: result.dropped ? paths.length : 0,
|
|
133
|
+
screenIds
|
|
134
|
+
};
|
|
135
|
+
} catch (error) {
|
|
136
|
+
// Logged, never thrown: see BEST EFFORT above.
|
|
137
|
+
console.error('[forms] announcing a publish failed', error);
|
|
138
|
+
return empty;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
//# sourceMappingURL=form-publish-announce.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/forms/src/lib/server/form-publish-announce.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 { nodesPlaceForm, screenRoutePathToUrl } from '@aglyn/aglyn/server'\nimport { dropPluginSiteCache } from '@aglyn/aglyn/plugin-manager/plugin-site-cache'\nimport {\n isLiveUsageCandidate,\n readUsageSources,\n screenIdsUsingComponentDeep,\n screenIdsUsingLayoutDeep,\n type UsageSources,\n} from '@aglyn/tenant-data-admin/server/live-page-usage'\n\n/**\n * How many documents per collection the placement scan reads — the bound the\n * platform's own publish scans use, so a form publish and a component publish\n * read the same amount of the same site.\n */\nconst SCAN_LIMIT = 2000\n\n/**\n * Every live screen whose rendered output contains the form `formId`, however\n * indirectly.\n *\n * The form twin of {@link screenIdsUsingComponentDeep}, and the same walk for\n * the same reason: a placed form is found by SEARCHING node trees, not by\n * matching a pointer, and it can sit on a screen, inside page chrome, or\n * inside a reusable component that a third component nests. Publishing a form\n * changes every page that renders it, so anything this misses keeps serving\n * the old fields for the whole revalidate window while the editor reports that\n * the live sites now serve the new design.\n *\n * The first level is the only thing that differs from the component walk: a\n * document uses a form by PLACING it. After that the fan-out is identical, so\n * it is delegated rather than restated — a component that holds the form is\n * reached by whatever reaches that component, and a layout by whatever renders\n * inside it.\n *\n * Pure, and separate from the Firestore read, so the closure is testable\n * without a database. Cycle-safe by delegation: both walks it defers to bound\n * themselves.\n */\nexport function screenIdsUsingFormDeep(\n formId: string,\n sources: UsageSources,\n): string[] {\n if (!formId) return []\n const screenIds = new Set<string>()\n\n for (const candidate of sources.screens) {\n if (!isLiveUsageCandidate(candidate)) continue\n if (nodesPlaceForm(candidate.nodes, formId)) screenIds.add(candidate.id)\n }\n for (const candidate of sources.layouts) {\n if (!isLiveUsageCandidate(candidate)) continue\n if (!nodesPlaceForm(candidate.nodes, formId)) continue\n // The layout itself renders no URL; the screens beneath it do.\n for (const screenId of screenIdsUsingLayoutDeep(\n candidate.id,\n sources.screens,\n sources.layouts,\n )) {\n screenIds.add(screenId)\n }\n }\n for (const candidate of sources.components) {\n if (!isLiveUsageCandidate(candidate)) continue\n if (!nodesPlaceForm(candidate.nodes, formId)) continue\n // And a component renders wherever it is placed, however deeply nested.\n for (const screenId of screenIdsUsingComponentDeep(candidate.id, sources)) {\n screenIds.add(screenId)\n }\n }\n\n return [...screenIds]\n}\n\n/**\n * Drop the cached pages that place `formId`, after its design was published.\n *\n * The plugin knows WHICH pages — the ones whose node trees place the form,\n * found by {@link screenIdsUsingFormDeep} over the site's screens, layouts and\n * components — and the app knows how to drop one on every address the site\n * answers at, so the addresses go through the platform's site cache\n * (`dropPluginSiteCache`, narrowed by `paths`).\n *\n * BEST EFFORT, ALWAYS. The publish has already succeeded by the time this\n * runs, so a failed cache hint must never make it look failed: every failure\n * resolves, and the render cache's TTL stays underneath as the backstop. A\n * scan the bound truncated drops the whole site instead, because a prefix of\n * the site would report a publish while leaving placed pages stale.\n */\nexport async function announceFormPublish(options: {\n firestore: FirebaseFirestore.Firestore\n hostId: string\n formId: string\n}): Promise<{ revalidated: number; screenIds: string[] }> {\n const { firestore, hostId, formId } = options\n const empty = { revalidated: 0, screenIds: [] as string[] }\n if (!hostId || !formId) return empty\n try {\n const hostRef = firestore.collection('hosts').doc(hostId)\n const [hostSnapshot, sources] = await Promise.all([\n hostRef.get(),\n readUsageSources(hostRef, SCAN_LIMIT),\n ])\n const reason = 'a form was published'\n if (sources.truncated) {\n const result = await dropPluginSiteCache({ hostIds: [hostId], reason })\n return { revalidated: result.dropped, screenIds: [] }\n }\n const screenIds = screenIdsUsingFormDeep(formId, sources.candidates)\n // The routing map is `screenId → path`; a screen not in it is not\n // routable, so it has no live page to drop.\n const routes = (hostSnapshot.get('screens') ?? {}) as Record<string, string>\n const paths = screenIds\n .map((id) => routes[id])\n .filter((path): path is string => Boolean(path))\n .map((path) => screenRoutePathToUrl(path))\n if (!paths.length) return { revalidated: 0, screenIds }\n const result = await dropPluginSiteCache({\n hostIds: [hostId],\n paths: { [hostId]: paths },\n reason,\n })\n return { revalidated: result.dropped ? paths.length : 0, screenIds }\n } catch (error) {\n // Logged, never thrown: see BEST EFFORT above.\n console.error('[forms] announcing a publish failed', error)\n return empty\n }\n}\n"],"names":["nodesPlaceForm","screenRoutePathToUrl","dropPluginSiteCache","isLiveUsageCandidate","readUsageSources","screenIdsUsingComponentDeep","screenIdsUsingLayoutDeep","SCAN_LIMIT","screenIdsUsingFormDeep","formId","sources","screenIds","Set","candidate","screens","nodes","add","id","layouts","screenId","components","announceFormPublish","options","firestore","hostId","empty","revalidated","hostSnapshot","hostRef","collection","doc","Promise","all","get","reason","truncated","result","hostIds","dropped","candidates","routes","paths","map","filter","path","Boolean","length","error","console"],"mappings":"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"}
|
package/src/lib/server.d.ts
CHANGED
|
@@ -40,3 +40,4 @@ import { type PluginApiHandler } from '@aglyn/aglyn/server';
|
|
|
40
40
|
export declare const formStatsHandler: PluginApiHandler;
|
|
41
41
|
/** Console API registration. */
|
|
42
42
|
export declare function registerFormsConsoleApi(): void;
|
|
43
|
+
export type { formsPlanEntitlements } from './plan-entitlements';
|
package/src/lib/server.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { firebaseAdmin, isImpersonationSession } from "@aglyn/tenant-data-admin";
|
|
18
18
|
import { isRefusedIdToken } from "@aglyn/tenant-data-admin/server/id-token-refusal";
|
|
19
19
|
import { FORM_STATS_RECOUNT_MAX, recountFormStats } from "./server/form-stats.js";
|
|
20
|
+
import { formPromoteHandler } from "./server/form-promote-route.js";
|
|
20
21
|
/**
|
|
21
22
|
* `POST /api/forms/stats` — recount the named forms' counters from their
|
|
22
23
|
* rows (AGL-3330).
|
|
@@ -117,6 +118,11 @@ import { FORM_STATS_RECOUNT_MAX, recountFormStats } from "./server/form-stats.js
|
|
|
117
118
|
};
|
|
118
119
|
/** Console API registration. */ export function registerFormsConsoleApi() {
|
|
119
120
|
registerPluginApiRoute('forms/stats', formStatsHandler);
|
|
121
|
+
// A form's publish — the `besignerDocuments` publish route
|
|
122
|
+
// `plugins.config.json` declares for it.
|
|
123
|
+
registerPluginApiRoute('forms/promote', {
|
|
124
|
+
web: formPromoteHandler
|
|
125
|
+
});
|
|
120
126
|
}
|
|
121
127
|
|
|
122
128
|
//# sourceMappingURL=server.js.map
|
package/src/lib/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/plugins/forms/src/lib/server.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 { registerPluginApiRoute, type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { firebaseAdmin, isImpersonationSession } from '@aglyn/tenant-data-admin'\nimport { isRefusedIdToken } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport { FORM_STATS_RECOUNT_MAX, recountFormStats } from './server/form-stats'\n\n/**\n * `POST /api/forms/stats` — recount the named forms' counters from their\n * rows (AGL-3330).\n *\n * A form's `stats` are server-written: the rules refuse a client write that\n * touches them, because a merchant who could set their own submission count\n * could make any lead rate read however they liked. So a CONSOLE act that\n * changes what they should say — deleting a submission from the Inbox,\n * switching lead routing (a lead-routing form holds `0` leads rather than\n * none) — asks for them to be recounted here, and `recountFormStats` counts\n * the rows and writes what they say.\n *\n * Nothing the caller sends is written: it names forms, and the figures come\n * from Firestore. That is why any member of the site may ask — a recount can\n * only ever put a form's counters in agreement with rows the member can\n * already list — and why it needs no role beyond membership. It is bounded at\n * {@link FORM_STATS_RECOUNT_MAX} forms a request, three aggregation reads\n * each, and a form already in agreement is not written.\n *\n * Body: `{ hostId, formIds: string[] }`. Answers each form's recount, or\n * `null` for an id that is not a form on the site.\n */\nexport const formStatsHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const hostId = typeof req.body?.hostId === 'string' ? req.body.hostId : ''\n const formIds = Array.isArray(req.body?.formIds)\n ? [\n ...new Set(\n (req.body.formIds as unknown[]).filter(\n (id): id is string => typeof id === 'string' && id.length > 0 && id.length <= 64,\n ),\n ),\n ]\n : []\n if (!hostId || !formIds.length) {\n return res.status(400).json({ error: 'Missing hostId or formIds' })\n }\n if (formIds.length > FORM_STATS_RECOUNT_MAX) {\n return res\n .status(400)\n .json({ error: `At most ${FORM_STATS_RECOUNT_MAX} forms a request` })\n }\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return res.status(403).json({ error: 'Verify your email address first' })\n }\n const firestore = firebaseAdmin.app().firestore()\n const host = await firestore.collection('hosts').doc(hostId).get()\n if (!host.exists) return res.status(404).json({ error: 'Unknown site' })\n const staff = decoded['staff'] === true\n const memberRole = (host.get('memberRoles') ?? {})[decoded.uid]\n if (!staff && !memberRole) {\n return res.status(403).json({ error: 'Not a member of this site' })\n }\n const recounts: Record<string, unknown> = {}\n for (const formId of formIds) {\n const recount = await recountFormStats({ firestore, hostId, formId })\n recounts[formId] = recount\n ? { recounted: recount.recounted, drift: recount.drift, written: recount.written }\n : null\n }\n return res.status(200).json({ ok: true, recounts })\n } catch (error) {\n // A refused credential is a 401; anything else is a fault of ours.\n if (isRefusedIdToken(error)) {\n return res.status(401).json({ error: 'Unauthenticated' })\n }\n console.error('[forms] stats recount failed', error)\n return res.status(500).json({ error: 'Recount failed' })\n }\n}\n\n/** Console API registration. */\nexport function registerFormsConsoleApi(): void {\n registerPluginApiRoute('forms/stats', formStatsHandler)\n}\n"],"names":["registerPluginApiRoute","firebaseAdmin","isImpersonationSession","isRefusedIdToken","FORM_STATS_RECOUNT_MAX","recountFormStats","formStatsHandler","req","res","method","status","json","error","hostId","body","formIds","Array","isArray","Set","filter","id","length","authorization","String","headers","idToken","startsWith","slice","undefined","host","decoded","app","auth","verifyIdToken","email_verified","firestore","collection","doc","get","exists","staff","memberRole","uid","recounts","formId","recount","recounted","drift","written","ok","console","registerFormsConsoleApi"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,sBAAsB,QAA+B,sBAAqB;AACnF,SAASC,aAAa,EAAEC,sBAAsB,QAAQ,2BAA0B;AAChF,SAASC,gBAAgB,QAAQ,mDAAkD;AACnF,SAASC,sBAAsB,EAAEC,gBAAgB,QAAQ,yBAAqB;
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/forms/src/lib/server.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 { registerPluginApiRoute, type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { firebaseAdmin, isImpersonationSession } from '@aglyn/tenant-data-admin'\nimport { isRefusedIdToken } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport { FORM_STATS_RECOUNT_MAX, recountFormStats } from './server/form-stats'\nimport { formPromoteHandler } from './server/form-promote-route'\n\n/**\n * `POST /api/forms/stats` — recount the named forms' counters from their\n * rows (AGL-3330).\n *\n * A form's `stats` are server-written: the rules refuse a client write that\n * touches them, because a merchant who could set their own submission count\n * could make any lead rate read however they liked. So a CONSOLE act that\n * changes what they should say — deleting a submission from the Inbox,\n * switching lead routing (a lead-routing form holds `0` leads rather than\n * none) — asks for them to be recounted here, and `recountFormStats` counts\n * the rows and writes what they say.\n *\n * Nothing the caller sends is written: it names forms, and the figures come\n * from Firestore. That is why any member of the site may ask — a recount can\n * only ever put a form's counters in agreement with rows the member can\n * already list — and why it needs no role beyond membership. It is bounded at\n * {@link FORM_STATS_RECOUNT_MAX} forms a request, three aggregation reads\n * each, and a form already in agreement is not written.\n *\n * Body: `{ hostId, formIds: string[] }`. Answers each form's recount, or\n * `null` for an id that is not a form on the site.\n */\nexport const formStatsHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const hostId = typeof req.body?.hostId === 'string' ? req.body.hostId : ''\n const formIds = Array.isArray(req.body?.formIds)\n ? [\n ...new Set(\n (req.body.formIds as unknown[]).filter(\n (id): id is string => typeof id === 'string' && id.length > 0 && id.length <= 64,\n ),\n ),\n ]\n : []\n if (!hostId || !formIds.length) {\n return res.status(400).json({ error: 'Missing hostId or formIds' })\n }\n if (formIds.length > FORM_STATS_RECOUNT_MAX) {\n return res\n .status(400)\n .json({ error: `At most ${FORM_STATS_RECOUNT_MAX} forms a request` })\n }\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return res.status(403).json({ error: 'Verify your email address first' })\n }\n const firestore = firebaseAdmin.app().firestore()\n const host = await firestore.collection('hosts').doc(hostId).get()\n if (!host.exists) return res.status(404).json({ error: 'Unknown site' })\n const staff = decoded['staff'] === true\n const memberRole = (host.get('memberRoles') ?? {})[decoded.uid]\n if (!staff && !memberRole) {\n return res.status(403).json({ error: 'Not a member of this site' })\n }\n const recounts: Record<string, unknown> = {}\n for (const formId of formIds) {\n const recount = await recountFormStats({ firestore, hostId, formId })\n recounts[formId] = recount\n ? { recounted: recount.recounted, drift: recount.drift, written: recount.written }\n : null\n }\n return res.status(200).json({ ok: true, recounts })\n } catch (error) {\n // A refused credential is a 401; anything else is a fault of ours.\n if (isRefusedIdToken(error)) {\n return res.status(401).json({ error: 'Unauthenticated' })\n }\n console.error('[forms] stats recount failed', error)\n return res.status(500).json({ error: 'Recount failed' })\n }\n}\n\n/** Console API registration. */\nexport function registerFormsConsoleApi(): void {\n registerPluginApiRoute('forms/stats', formStatsHandler)\n // A form's publish — the `besignerDocuments` publish route\n // `plugins.config.json` declares for it.\n registerPluginApiRoute('forms/promote', { web: formPromoteHandler })\n}\n\n// Type-only (AGL-3080): the plugin's entitlement keys, declared by module\n// augmentation, for every program that loads this entry point.\nexport type { formsPlanEntitlements } from './plan-entitlements'\n"],"names":["registerPluginApiRoute","firebaseAdmin","isImpersonationSession","isRefusedIdToken","FORM_STATS_RECOUNT_MAX","recountFormStats","formPromoteHandler","formStatsHandler","req","res","method","status","json","error","hostId","body","formIds","Array","isArray","Set","filter","id","length","authorization","String","headers","idToken","startsWith","slice","undefined","host","decoded","app","auth","verifyIdToken","email_verified","firestore","collection","doc","get","exists","staff","memberRole","uid","recounts","formId","recount","recounted","drift","written","ok","console","registerFormsConsoleApi","web"],"mappings":"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"}
|
|
@@ -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 { PluginUsageAxesDeclaration } from '@aglyn/aglyn/plugin-manager/plugin-usage-axes';
|
|
18
|
+
/**
|
|
19
|
+
* THE FORMS PLUGIN'S METER (AGL-3080): form submissions, in the platform's
|
|
20
|
+
* cost model and utilization table. Compiled into core by the manifest
|
|
21
|
+
* generator (`register.usageAxes`).
|
|
22
|
+
*
|
|
23
|
+
* Priced at `perFormSubmission`, one of the three rates the customer is
|
|
24
|
+
* billed at cost × 1.30 — the rate itself stays core's, beside the billed
|
|
25
|
+
* table it is reconciled against. The band is per SITE, so the org-wide band
|
|
26
|
+
* is the plan's figure times `hostLimit`, and it is measured by the per-site
|
|
27
|
+
* counter `/api/forms/submit` writes.
|
|
28
|
+
*
|
|
29
|
+
* Its overage is WITHHELD while `release_inbox` is off for the workspace
|
|
30
|
+
* (AGL-1688): submissions keep arriving and the API keeps serving them, but
|
|
31
|
+
* nobody is charged for a lead list the console gives them no way to read.
|
|
32
|
+
* The units are still counted, and what was forgone is recorded.
|
|
33
|
+
*/
|
|
34
|
+
export declare function formsUsageAxes(): PluginUsageAxesDeclaration;
|
|
@@ -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
|
+
* THE FORMS PLUGIN'S METER (AGL-3080): form submissions, in the platform's
|
|
18
|
+
* cost model and utilization table. Compiled into core by the manifest
|
|
19
|
+
* generator (`register.usageAxes`).
|
|
20
|
+
*
|
|
21
|
+
* Priced at `perFormSubmission`, one of the three rates the customer is
|
|
22
|
+
* billed at cost × 1.30 — the rate itself stays core's, beside the billed
|
|
23
|
+
* table it is reconciled against. The band is per SITE, so the org-wide band
|
|
24
|
+
* is the plan's figure times `hostLimit`, and it is measured by the per-site
|
|
25
|
+
* counter `/api/forms/submit` writes.
|
|
26
|
+
*
|
|
27
|
+
* Its overage is WITHHELD while `release_inbox` is off for the workspace
|
|
28
|
+
* (AGL-1688): submissions keep arriving and the API keeps serving them, but
|
|
29
|
+
* nobody is charged for a lead list the console gives them no way to read.
|
|
30
|
+
* The units are still counted, and what was forgone is recorded.
|
|
31
|
+
*/ export function formsUsageAxes() {
|
|
32
|
+
return {
|
|
33
|
+
costAxes: [
|
|
34
|
+
{
|
|
35
|
+
id: 'formSubmissions',
|
|
36
|
+
order: 30,
|
|
37
|
+
fields: [
|
|
38
|
+
'formSubmissions'
|
|
39
|
+
],
|
|
40
|
+
rate: 'perFormSubmission'
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
bands: [
|
|
44
|
+
{
|
|
45
|
+
id: 'formSubmissions',
|
|
46
|
+
label: 'Form submissions',
|
|
47
|
+
order: 40,
|
|
48
|
+
fields: [
|
|
49
|
+
'formSubmissions'
|
|
50
|
+
],
|
|
51
|
+
entitlement: 'formSubmissionsPerMonth',
|
|
52
|
+
perHost: true,
|
|
53
|
+
hostCounter: 'formSubmissions',
|
|
54
|
+
// An infrastructure meter: what a workspace receives past its band is
|
|
55
|
+
// billed at cost × 1.30 beside storage and bandwidth — while the Inbox
|
|
56
|
+
// page, where submissions are read, is released to it.
|
|
57
|
+
metered: {
|
|
58
|
+
rate: 'perFormSubmission',
|
|
59
|
+
quotedPer: 1000,
|
|
60
|
+
noun: 'form submissions',
|
|
61
|
+
withheldUntil: 'release_inbox'
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
//# sourceMappingURL=usage-axes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/forms/src/lib/usage-axes.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 { PluginUsageAxesDeclaration } from '@aglyn/aglyn/plugin-manager/plugin-usage-axes'\n\n/**\n * THE FORMS PLUGIN'S METER (AGL-3080): form submissions, in the platform's\n * cost model and utilization table. Compiled into core by the manifest\n * generator (`register.usageAxes`).\n *\n * Priced at `perFormSubmission`, one of the three rates the customer is\n * billed at cost × 1.30 — the rate itself stays core's, beside the billed\n * table it is reconciled against. The band is per SITE, so the org-wide band\n * is the plan's figure times `hostLimit`, and it is measured by the per-site\n * counter `/api/forms/submit` writes.\n *\n * Its overage is WITHHELD while `release_inbox` is off for the workspace\n * (AGL-1688): submissions keep arriving and the API keeps serving them, but\n * nobody is charged for a lead list the console gives them no way to read.\n * The units are still counted, and what was forgone is recorded.\n */\nexport function formsUsageAxes(): PluginUsageAxesDeclaration {\n return {\n costAxes: [\n {\n id: 'formSubmissions',\n order: 30,\n fields: ['formSubmissions'],\n rate: 'perFormSubmission',\n },\n ],\n bands: [\n {\n id: 'formSubmissions',\n label: 'Form submissions',\n order: 40,\n fields: ['formSubmissions'],\n entitlement: 'formSubmissionsPerMonth',\n perHost: true,\n hostCounter: 'formSubmissions',\n // An infrastructure meter: what a workspace receives past its band is\n // billed at cost × 1.30 beside storage and bandwidth — while the Inbox\n // page, where submissions are read, is released to it.\n metered: {\n rate: 'perFormSubmission',\n quotedPer: 1000,\n noun: 'form submissions',\n withheldUntil: 'release_inbox',\n },\n },\n ],\n }\n}\n"],"names":["formsUsageAxes","costAxes","id","order","fields","rate","bands","label","entitlement","perHost","hostCounter","metered","quotedPer","noun","withheldUntil"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAID;;;;;;;;;;;;;;;CAeC,GACD,OAAO,SAASA;IACd,OAAO;QACLC,UAAU;YACR;gBACEC,IAAI;gBACJC,OAAO;gBACPC,QAAQ;oBAAC;iBAAkB;gBAC3BC,MAAM;YACR;SACD;QACDC,OAAO;YACL;gBACEJ,IAAI;gBACJK,OAAO;gBACPJ,OAAO;gBACPC,QAAQ;oBAAC;iBAAkB;gBAC3BI,aAAa;gBACbC,SAAS;gBACTC,aAAa;gBACb,sEAAsE;gBACtE,uEAAuE;gBACvE,uDAAuD;gBACvDC,SAAS;oBACPN,MAAM;oBACNO,WAAW;oBACXC,MAAM;oBACNC,eAAe;gBACjB;YACF;SACD;IACH;AACF"}
|