@bongos/core 1.20.48 → 1.20.50
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/.bongos-core.json +84 -54
- package/.claude/skills/new-project/SKILL.md +5 -0
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/api/openapi.json +3 -0
- package/docs/api-reference.md +1 -1
- package/docs/copy-inventory.md +434 -400
- package/docs/copy-registry.json +816 -508
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +11 -2
- package/docs/page-readings.json +733 -690
- package/modules/hall-ui/public/criterion-uat-ui.js +280 -0
- package/modules/hall-ui/public/goal-map.html +5 -3
- package/modules/hall-ui/public/goals-page.js +23 -44
- package/modules/hall-ui/public/goals-render.js +24 -15
- package/modules/hall-ui/public/goals.css +45 -0
- package/modules/hall-ui/public/goals.html +5 -2
- package/modules/hall-ui/public/goals.states.json +38 -0
- package/modules/hall-ui/public/sky-data.js +9 -1
- package/modules/hall-ui/public/sky-interaction.js +1 -1
- package/modules/hall-ui/public/sky-panel.js +1 -1
- package/modules/hall-ui/public/thinking.html +5 -3
- package/modules/lifecycle/done-when.js +3 -1
- package/modules/lifecycle/sky.js +23 -8
- package/modules/platform-identity/routes/sso.js +5 -0
- package/modules/provisioning/migrations/provisioning_033_terms_ack.sql +33 -0
- package/modules/provisioning/onboard-plan.js +5 -0
- package/modules/provisioning/provisioning.js +7 -4
- package/modules/provisioning/routes/provisioning.js +3 -3
- package/modules/provisioning/terms-ack.js +36 -0
- package/modules/public-landing/public/projects.html +226 -77
- package/modules/public-landing/public/projects.probes.json +8 -2
- package/modules/public-landing/public/projects.states.json +16 -12
- package/modules/ui-design/kit/serve.js +6 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/hall-preview/server.js +2 -0
- package/src/module-api.js +1 -1
- package/tests/claim_action.mjs +6 -1
- package/tests/criterion_uat_ui.mjs +113 -0
- package/tests/goal_criterion_confirm_ui.mjs +188 -91
- package/tests/goal_map_page.mjs +2 -1
- package/tests/hall_sky.mjs +16 -0
- package/tests/helpers.mjs +3 -1
- package/tests/projects_hub.mjs +4 -2
- package/tests/projects_hub_app_status.mjs +1 -1
- package/tests/projects_hub_app_step.mjs +9 -9
- package/tests/projects_hub_module_picker.mjs +7 -7
- package/tests/projects_hub_pre_uat.mjs +4 -1
- package/tests/provisioning_terms_ack.mjs +216 -0
- package/tests/sky_read.mjs +7 -1
- package/tests/wizard_draft_resume.mjs +4 -4
- package/tests/wizard_front_door.mjs +271 -0
- package/tests/wizard_intent_resume.mjs +31 -19
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
${waiting.length ? fact('waits on', waiting.map(name).join(', ')) : ''}
|
|
79
79
|
${waitedOn.length ? fact('waited on by', waitedOn.map(name).join(', ')) : ''}
|
|
80
80
|
</div>
|
|
81
|
-
${g.crit.length ? `<ul class="crit">${g.crit.slice(0, 10).map((c) => `<li><span class="ring${c.satisfied ? ' on' : ''}"></span><span>${esc(plain(c.text).slice(0, 120))}${plain(c.text).length > 120 ? '…' : ''}${c.bornOf ? ` <span class="born">· born of a thought</span>` : ''}${c.credits ? ` <span class="lbl">${c.credits} cr</span>` : ''}</span></li>`).join('')}</ul>` : ''}
|
|
81
|
+
${g.crit.length ? `<ul class="crit">${g.crit.slice(0, 10).map((c) => `<li><span class="ring${c.satisfied ? ' on' : ''}"></span><span>${esc(plain(c.text).slice(0, 120))}${plain(c.text).length > 120 ? '…' : ''}${c.bornOf ? ` <span class="born">· born of a thought</span>` : ''}${c.credits ? ` <span class="lbl">${c.credits} cr</span>` : ''}${c.uatLabel ? ` <span class="lbl">${esc(c.uatLabel)}</span>` : ''}${c.backendOnly ? ' <span class="lbl">backend-only</span>' : ''}</span></li>`).join('')}</ul>` : ''}
|
|
82
82
|
<div class="doors">
|
|
83
83
|
<button type="button" class="door primary" id="descendBtn">${g.status === 'open' ? 'descend into the well' : 'look closer'}</button>
|
|
84
84
|
${edgesOf(g).length ? `<button type="button" class="door" id="chartBtn">chart its routes</button>` : ''}
|
|
@@ -142,11 +142,13 @@
|
|
|
142
142
|
<script src="/builders/palette.js" defer></script>
|
|
143
143
|
|
|
144
144
|
<script src="/builders/idea-objects.js?v=2026-09-27-split"></script>
|
|
145
|
-
|
|
145
|
+
<!-- task 1004402: the UAT state words the rings share with the goal page. -->
|
|
146
|
+
<script src="/builders/criterion-uat-ui.js?v=2026-09-30-uat1"></script>
|
|
147
|
+
<script src="/builders/sky-data.js?v=2026-09-30-uat1"></script>
|
|
146
148
|
<script src="/builders/sky-layout.js?v=2026-09-27-split"></script>
|
|
147
149
|
<script src="/builders/sky-draw.js?v=2026-09-27-split"></script>
|
|
148
|
-
<script src="/builders/sky-interaction.js?v=2026-09-
|
|
149
|
-
<script src="/builders/sky-panel.js?v=2026-09-
|
|
150
|
+
<script src="/builders/sky-interaction.js?v=2026-09-30-uat1"></script>
|
|
151
|
+
<script src="/builders/sky-panel.js?v=2026-09-30-uat1"></script>
|
|
150
152
|
<script src="/builders/sky-composer.js?v=2026-09-27-split"></script>
|
|
151
153
|
<script src="/builders/sky-tour.js?v=2026-09-27-split"></script>
|
|
152
154
|
</body>
|
|
@@ -221,7 +221,9 @@ async function criterionProgress(versionId) {
|
|
|
221
221
|
// about coverage. It uses idx_tasks_version_status (version_id leading).
|
|
222
222
|
const [{ rows }, { rows: unattr }, goalRows] = await Promise.all([
|
|
223
223
|
pool.query(
|
|
224
|
-
|
|
224
|
+
// MATERIALIZED (task 1004402): the per-criterion UAT subqueries run once per
|
|
225
|
+
// criterion here, never once per (criterion, task) row after the join.
|
|
226
|
+
`WITH ranked AS MATERIALIZED (
|
|
225
227
|
SELECT d.id, d.version_id, d.criterion_id, d.criterion_md, d.satisfied, d.goal_id,
|
|
226
228
|
ROW_NUMBER() OVER (ORDER BY d.sort_order, d.criterion_id) AS cnum,
|
|
227
229
|
${uatReadColumnsSql('d')}
|
package/modules/lifecycle/sky.js
CHANGED
|
@@ -42,7 +42,7 @@ const api = require('../../src/module-api');
|
|
|
42
42
|
const { pool } = api;
|
|
43
43
|
const goalGraph = require('./goal-graph');
|
|
44
44
|
const goalCategory = require('./goal-category');
|
|
45
|
-
const { isPendingReview } = require('./done-when');
|
|
45
|
+
const { isPendingReview, uatState, uatReadColumnsSql } = require('./done-when');
|
|
46
46
|
const { TERMINAL_STATUSES } = require('./task-status');
|
|
47
47
|
|
|
48
48
|
const SKY_CACHE_TTL_MS = 60 * 1000;
|
|
@@ -67,15 +67,24 @@ async function skyRows({ versionId = null, viewerId = null, viewerIsArchon = fal
|
|
|
67
67
|
activePool.query(
|
|
68
68
|
// The criterion→task join, read once. Flat rows on purpose (see the header):
|
|
69
69
|
// the fold happens in shapeSky so a test can compare it with the rollup's.
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
// task 1004402: each criterion's UAT columns (ADR 0351) are read ONCE per
|
|
71
|
+
// criterion, in a MATERIALIZED step before the task join fans the rows out;
|
|
72
|
+
// spliced into the flat join they re-ran once per linked task, on the one
|
|
73
|
+
// read that is the whole project's sky.
|
|
74
|
+
`WITH crit AS MATERIALIZED (
|
|
75
|
+
SELECT c.id, c.goal_id, c.criterion_id, c.criterion_md, c.satisfied, c.sort_order,
|
|
76
|
+
${uatReadColumnsSql('c')}
|
|
77
|
+
FROM done_when_criteria c
|
|
78
|
+
JOIN goals g ON g.id = c.goal_id
|
|
79
|
+
WHERE c.goal_id IS NOT NULL
|
|
80
|
+
AND ($1::text IS NULL OR g.version_id = $1)
|
|
81
|
+
)
|
|
82
|
+
SELECT c.id::text AS id, c.goal_id::text AS goal_id, c.criterion_id, c.criterion_md,
|
|
83
|
+
c.satisfied, c.sort_order, c.uat_kind, c.backend_only,
|
|
72
84
|
t.id::text AS task_id, t.status AS task_status
|
|
73
|
-
FROM
|
|
74
|
-
JOIN goals g ON g.id = c.goal_id
|
|
85
|
+
FROM crit c
|
|
75
86
|
LEFT JOIN task_criteria tc ON tc.criterion_id = c.id
|
|
76
87
|
LEFT JOIN tasks t ON t.id = tc.task_id
|
|
77
|
-
WHERE c.goal_id IS NOT NULL
|
|
78
|
-
AND ($1::text IS NULL OR g.version_id = $1)
|
|
79
88
|
ORDER BY c.goal_id, c.sort_order, c.criterion_id, t.id`,
|
|
80
89
|
[version]
|
|
81
90
|
),
|
|
@@ -111,21 +120,27 @@ function foldCriteria(rows = []) {
|
|
|
111
120
|
criterion_id: row.criterion_id,
|
|
112
121
|
criterion_md: row.criterion_md,
|
|
113
122
|
satisfied: row.satisfied === true,
|
|
123
|
+
backend_only: row.backend_only === true,
|
|
114
124
|
task_ids: [],
|
|
115
125
|
_unshipped: 0,
|
|
126
|
+
_shipped: 0,
|
|
127
|
+
_uatKind: row.uat_kind ?? null,
|
|
116
128
|
};
|
|
117
129
|
byId.set(c.id, c);
|
|
118
130
|
}
|
|
119
131
|
if (row.task_id != null) {
|
|
120
132
|
c.task_ids.push(String(row.task_id));
|
|
121
133
|
if (!TERMINAL_STATUSES.includes(row.task_status)) c._unshipped += 1;
|
|
134
|
+
if (row.task_status === 'shipped') c._shipped += 1;
|
|
122
135
|
}
|
|
123
136
|
}
|
|
124
137
|
return [...byId.values()].map((c) => {
|
|
125
|
-
const { _unshipped, ...rest } = c;
|
|
138
|
+
const { _unshipped, _shipped, _uatKind, ...rest } = c;
|
|
126
139
|
return {
|
|
127
140
|
...rest,
|
|
128
141
|
pending_review: isPendingReview({ satisfied: c.satisfied, linked: c.task_ids.length, unshipped: _unshipped }),
|
|
142
|
+
// task 1004402: the same derivation the goal page and /status use.
|
|
143
|
+
uat_state: uatState({ satisfied: c.satisfied, linked: c.task_ids.length, unshipped: _unshipped, shipped: _shipped, latestKind: _uatKind }),
|
|
129
144
|
};
|
|
130
145
|
});
|
|
131
146
|
}
|
|
@@ -25,6 +25,7 @@ const hubKeys = require('../hub-keys');
|
|
|
25
25
|
const hubDevice = require('../hub-device');
|
|
26
26
|
// The D7 applicant-profile read (task 1002972) and the echo that gates it.
|
|
27
27
|
const applicantProfile = require('../applicant-profile');
|
|
28
|
+
const terms = require('../terms-acceptance');
|
|
28
29
|
const applicationEcho = require('../application-echo');
|
|
29
30
|
const instanceFeedback = require('../instance-feedback');
|
|
30
31
|
|
|
@@ -204,6 +205,10 @@ function registerSeams() {
|
|
|
204
205
|
// Floors on the EXISTENCE predicate, not the rollup one: hide_stats
|
|
205
206
|
// suppresses numbers, never addressability.
|
|
206
207
|
getPublicHandle: (githubId) => canonicalProfile.getPublicHandle(githubId, { pool }),
|
|
208
|
+
// task 1004417 (BV2.PS05): the terms version /terms states, read by provisioning
|
|
209
|
+
// when a founder ticks the create wizard's terms box. A READ of the constant only —
|
|
210
|
+
// it stamps nothing on any account (that stays terms-acceptance.js's alone).
|
|
211
|
+
termsVersion: () => terms.TERMS_VERSION,
|
|
207
212
|
});
|
|
208
213
|
}
|
|
209
214
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
-- provisioning_033_terms_ack.sql — the founder's terms acknowledgement, kept on the project
|
|
2
|
+
-- (task 1004417, BV2.PS05; goal 1000121, spec docs/specs/bongos-v2-project-startup.md).
|
|
3
|
+
--
|
|
4
|
+
-- WHY THIS EXISTS. The create wizard now opens on a terms screen: the founder ticks "I can
|
|
5
|
+
-- sign legally, or can get a signature, for the use of this platform, and I acknowledge the
|
|
6
|
+
-- terms and conditions" before anything else is asked. The owner chose (2026-09-30) to keep
|
|
7
|
+
-- that record ON THE PROJECT, not on the person's platform account: the account-level stamp
|
|
8
|
+
-- is ADR 0213's, written only at a witnessed sign-in (and task 1003361 adds the hub's own
|
|
9
|
+
-- sign-in as its second capture point). This one says who founded WHICH project under WHICH
|
|
10
|
+
-- text, which a later terms revision needs to know per project.
|
|
11
|
+
--
|
|
12
|
+
-- WHAT IT ADDS.
|
|
13
|
+
-- provisioning_instances.terms_ack_at — when the create that ticked the box was filed.
|
|
14
|
+
-- provisioning_instances.terms_ack_version — the terms version the SERVER stamped: read from
|
|
15
|
+
-- platform-identity's TERMS_VERSION through the platform-identity.hub port, never from the
|
|
16
|
+
-- request body, so a caller cannot record agreement to a text it names itself. NULL on an
|
|
17
|
+
-- instance with no hub to ask (the acknowledgement time is still kept).
|
|
18
|
+
--
|
|
19
|
+
-- The names are deliberately NOT terms_accepted_at / terms_version: those are the account's
|
|
20
|
+
-- acceptance columns, and tests/terms_acceptance.mjs fails any second server file that writes
|
|
21
|
+
-- them. This is a different record with a different owner.
|
|
22
|
+
--
|
|
23
|
+
-- Nullable, no backfill: a project created before this shipped never saw the screen, and a
|
|
24
|
+
-- NULL says exactly that. Written once, at create; an idempotent re-request never rewrites it.
|
|
25
|
+
--
|
|
26
|
+
-- Additive and namespaced (ADR 0083): no down-migration, idempotent — safe to re-run.
|
|
27
|
+
|
|
28
|
+
BEGIN;
|
|
29
|
+
|
|
30
|
+
ALTER TABLE provisioning_instances ADD COLUMN IF NOT EXISTS terms_ack_at timestamptz;
|
|
31
|
+
ALTER TABLE provisioning_instances ADD COLUMN IF NOT EXISTS terms_ack_version text;
|
|
32
|
+
|
|
33
|
+
COMMIT;
|
|
@@ -26,6 +26,11 @@
|
|
|
26
26
|
// an existing repo; ADR 0121 §Decision 1 / task 2046). Every other leg is identical, so
|
|
27
27
|
// `mode` only relabels the github-repo / init / scaffold steps.
|
|
28
28
|
//
|
|
29
|
+
// The hub wizard's FRONT DOOR (task 1004417 — the terms tick, then demo or create) is
|
|
30
|
+
// deliberately NOT a step here: it comes before the rail, sets up nothing, and its record
|
|
31
|
+
// is the project's own terms columns (./terms-ack.js), not a plan key. The /new-project
|
|
32
|
+
// skill's "step 0" says the same for the other forms.
|
|
33
|
+
//
|
|
29
34
|
// `opts.domain` (task 1002703): the terminal enter-hall step names the instance's own
|
|
30
35
|
// hall address when the renderer knows the domain, and falls back to the `bongos dev`
|
|
31
36
|
// local-hall one-liner when it doesn't — the hall is the point of creating a project,
|
|
@@ -1076,7 +1076,7 @@ async function listAllInstances(db, { status } = {}) {
|
|
|
1076
1076
|
// re-fetches the existing row and returns { created:false } so the ROUTE can
|
|
1077
1077
|
// decide (same owner → idempotent re-request; different owner → 409). Never
|
|
1078
1078
|
// silently reassigns ownership.
|
|
1079
|
-
async function createInstance(db, { ownerBuilderId, slug, targetRef, hostingShape, domain, dbName, port, tier, onboardMode, type, description, detail, modules }) {
|
|
1079
|
+
async function createInstance(db, { ownerBuilderId, slug, targetRef, hostingShape, domain, dbName, port, tier, onboardMode, type, description, detail, modules, termsAck }) {
|
|
1080
1080
|
// Unknown/absent coerces to the shape that costs nothing and trusts nothing. Was
|
|
1081
1081
|
// 'co-tenant' (then the free front door); that shape is now retired + unrequestable,
|
|
1082
1082
|
// so coercing to it would mint a row no caller is allowed to ask for.
|
|
@@ -1103,12 +1103,15 @@ async function createInstance(db, { ownerBuilderId, slug, targetRef, hostingShap
|
|
|
1103
1103
|
try {
|
|
1104
1104
|
const { rows } = await db.query(
|
|
1105
1105
|
`INSERT INTO provisioning_instances
|
|
1106
|
-
(owner_builder_id, slug, target_ref, hosting_shape, domain, db_name, port, tier, cost_estimate_usd, onboard_mode, type, description, detail, modules)
|
|
1107
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13::jsonb, $14::jsonb)
|
|
1106
|
+
(owner_builder_id, slug, target_ref, hosting_shape, domain, db_name, port, tier, cost_estimate_usd, onboard_mode, type, description, detail, modules, terms_ack_at, terms_ack_version)
|
|
1107
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13::jsonb, $14::jsonb, CASE WHEN $15::boolean THEN now() END, $16)
|
|
1108
1108
|
RETURNING *`,
|
|
1109
1109
|
[ownerBuilderId, slug, targetRef ?? null, shape, domain ?? null,
|
|
1110
1110
|
dbName ?? null, port ?? null, tier ?? null, cost, mode, declaredType,
|
|
1111
|
-
told, JSON.stringify(answers), picked === null ? null : JSON.stringify(picked)
|
|
1111
|
+
told, JSON.stringify(answers), picked === null ? null : JSON.stringify(picked),
|
|
1112
|
+
// the wizard's terms screen (task 1004417, ./terms-ack.js): the route resolved the
|
|
1113
|
+
// version server-side; no acknowledgement leaves both columns NULL
|
|
1114
|
+
!!termsAck, (termsAck && termsAck.version) || null]
|
|
1112
1115
|
);
|
|
1113
1116
|
return { instance: rows[0], created: true };
|
|
1114
1117
|
} catch (e) {
|
|
@@ -56,7 +56,7 @@ const { recommendationsForDetails } = require('../recommendations');
|
|
|
56
56
|
const { makeAskRateLimiter, clientIp } = require('../rate-limit');
|
|
57
57
|
const { requireAuthorityForShape } = require('../paid-shape-gate');
|
|
58
58
|
const { requireRoomOnBox } = require('../capacity-gate'); // a full box refused up front (task 1003938)
|
|
59
|
-
const { adoptCredentialFault } = require('../credential-preflight');
|
|
59
|
+
const { adoptCredentialFault } = require('../credential-preflight'); const { termsAcknowledgement } = require('../terms-ack');
|
|
60
60
|
const catalogBridge = require('../catalog-bridge');
|
|
61
61
|
const capacity = require('../capacity'); // the box's slot ceiling (ADR 0285, task 1003927)
|
|
62
62
|
const { callbackPage } = require('./callback-page');
|
|
@@ -323,7 +323,7 @@ module.exports = function provisioningRoutes() {
|
|
|
323
323
|
// exactly the set the panel preselected. An explicit [] is the different
|
|
324
324
|
// answer "none of them". maxItems is a sanity ceiling well above a catalog
|
|
325
325
|
// that is three modules wide today.
|
|
326
|
-
modules: { type: 'array', itemsType: 'string', maxItems: 50 },
|
|
326
|
+
modules: { type: 'array', itemsType: 'string', maxItems: 50 }, terms_acknowledged: { type: 'boolean' }, // the latter: the wizard's terms tick (task 1004417, ../terms-ack.js)
|
|
327
327
|
})) return;
|
|
328
328
|
const body = req.body || {};
|
|
329
329
|
const detailFault = badProjectDetail(body.detail);
|
|
@@ -426,7 +426,7 @@ module.exports = function provisioningRoutes() {
|
|
|
426
426
|
description: body.description, detail: body.detail,
|
|
427
427
|
// The picker's selection (task 1002339) — absent for the same reason and
|
|
428
428
|
// with the same standing: skipped, and resolved from the bundle on read.
|
|
429
|
-
modules: body.modules,
|
|
429
|
+
modules: body.modules, termsAck: termsAcknowledgement(body),
|
|
430
430
|
});
|
|
431
431
|
if (req.releaseFreePlace) await req.releaseFreePlace(); // the row exists: the one-free lock's work is done (../free-place-lock.js)
|
|
432
432
|
// Slug already taken by ANOTHER builder — refuse (never reassign ownership).
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// modules/provisioning/terms-ack.js — the founder's terms acknowledgement, as the create
|
|
2
|
+
// route records it (task 1004417, BV2.PS05; migration provisioning_033).
|
|
3
|
+
//
|
|
4
|
+
// The wizard's first screen asks the founder to tick one box before anything else. The
|
|
5
|
+
// request carries only that the box was ticked (`terms_acknowledged: true`). WHICH terms
|
|
6
|
+
// is the server's answer, never the caller's: the version is read from platform-identity's
|
|
7
|
+
// TERMS_VERSION — the same constant /terms states and ADR 0213 stamps on an account —
|
|
8
|
+
// through the platform-identity.hub port. A body cannot name the text it agreed to, so a
|
|
9
|
+
// forged version is not a thing this record can hold.
|
|
10
|
+
//
|
|
11
|
+
// resolveOptional, not a `consumes` entry: platform-identity is hub-only (`default:
|
|
12
|
+
// false`), so an instance running provisioning without it has no terms version to ask for.
|
|
13
|
+
// The acknowledgement is still kept there, with a NULL version, rather than dropped —
|
|
14
|
+
// the founder did tick the box.
|
|
15
|
+
//
|
|
16
|
+
// Its own file because routes/provisioning.js sits a few lines under the 1500-line
|
|
17
|
+
// fitness ratchet.
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const api = require('../../src/module-api');
|
|
21
|
+
|
|
22
|
+
function currentTermsVersion() {
|
|
23
|
+
const hub = api.resolveOptional('platform-identity.hub');
|
|
24
|
+
const v = hub && typeof hub.termsVersion === 'function' ? hub.termsVersion() : null;
|
|
25
|
+
return typeof v === 'string' && v ? v : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// The record createInstance writes, or null when the box was not ticked (an older client,
|
|
29
|
+
// the CLI, an adopt run from a script — none of them showed the screen, and a NULL row
|
|
30
|
+
// says exactly that). Strictly `true`: the validator already refuses a non-boolean.
|
|
31
|
+
function termsAcknowledgement(body) {
|
|
32
|
+
if (!body || body.terms_acknowledged !== true) return null;
|
|
33
|
+
return { version: currentTermsVersion() };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { termsAcknowledgement };
|