@tellescope/sdk 1.255.18 → 1.255.19
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/lib/cjs/sdk.d.ts +7 -2
- package/lib/cjs/sdk.d.ts.map +1 -1
- package/lib/cjs/sdk.js +1 -0
- package/lib/cjs/sdk.js.map +1 -1
- package/lib/cjs/tests/api_tests/auto_merge_form_submission.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/auto_merge_form_submission.test.js +18 -8
- package/lib/cjs/tests/api_tests/auto_merge_form_submission.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/healthie_multi_integration.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/healthie_multi_integration.test.js +203 -72
- package/lib/cjs/tests/api_tests/healthie_multi_integration.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/phone_calls_conference_hold.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/phone_calls_conference_hold.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/phone_calls_conference_hold.test.js +102 -0
- package/lib/cjs/tests/api_tests/phone_calls_conference_hold.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/scoped_api_keys.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/scoped_api_keys.test.js +84 -81
- package/lib/cjs/tests/api_tests/scoped_api_keys.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/F-0106-F-0110-enduser-write-restrictions.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/security/F-0106-F-0110-enduser-write-restrictions.test.js +121 -78
- package/lib/cjs/tests/api_tests/security/F-0106-F-0110-enduser-write-restrictions.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.js +773 -0
- package/lib/cjs/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/enduser-ai-summary-trust-gate.test.d.ts +17 -0
- package/lib/cjs/tests/api_tests/security/enduser-ai-summary-trust-gate.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/enduser-ai-summary-trust-gate.test.js +215 -0
- package/lib/cjs/tests/api_tests/security/enduser-ai-summary-trust-gate.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/formsort_webhook_auth_field_lock.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/security/formsort_webhook_auth_field_lock.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js +1200 -0
- package/lib/cjs/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/public_endpoint_auth_field_lock.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/security/public_endpoint_auth_field_lock.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/public_endpoint_auth_field_lock.test.js +2330 -0
- package/lib/cjs/tests/api_tests/security/public_endpoint_auth_field_lock.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/tickets_bulk_assign_care_team.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/tickets_bulk_assign_care_team.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/tickets_bulk_assign_care_team.test.js +204 -0
- package/lib/cjs/tests/api_tests/tickets_bulk_assign_care_team.test.js.map +1 -0
- package/lib/cjs/tests/tests.d.ts.map +1 -1
- package/lib/cjs/tests/tests.js +211 -178
- package/lib/cjs/tests/tests.js.map +1 -1
- package/lib/esm/sdk.d.ts +7 -2
- package/lib/esm/sdk.d.ts.map +1 -1
- package/lib/esm/sdk.js +1 -0
- package/lib/esm/sdk.js.map +1 -1
- package/lib/esm/tests/api_tests/auto_merge_form_submission.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/auto_merge_form_submission.test.js +18 -8
- package/lib/esm/tests/api_tests/auto_merge_form_submission.test.js.map +1 -1
- package/lib/esm/tests/api_tests/healthie_multi_integration.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/healthie_multi_integration.test.js +205 -74
- package/lib/esm/tests/api_tests/healthie_multi_integration.test.js.map +1 -1
- package/lib/esm/tests/api_tests/phone_calls_conference_hold.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/phone_calls_conference_hold.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/phone_calls_conference_hold.test.js +98 -0
- package/lib/esm/tests/api_tests/phone_calls_conference_hold.test.js.map +1 -0
- package/lib/esm/tests/api_tests/scoped_api_keys.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/scoped_api_keys.test.js +84 -81
- package/lib/esm/tests/api_tests/scoped_api_keys.test.js.map +1 -1
- package/lib/esm/tests/api_tests/security/F-0106-F-0110-enduser-write-restrictions.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/security/F-0106-F-0110-enduser-write-restrictions.test.js +121 -78
- package/lib/esm/tests/api_tests/security/F-0106-F-0110-enduser-write-restrictions.test.js.map +1 -1
- package/lib/esm/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.js +766 -0
- package/lib/esm/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/enduser-ai-summary-trust-gate.test.d.ts +17 -0
- package/lib/esm/tests/api_tests/security/enduser-ai-summary-trust-gate.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/enduser-ai-summary-trust-gate.test.js +211 -0
- package/lib/esm/tests/api_tests/security/enduser-ai-summary-trust-gate.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/formsort_webhook_auth_field_lock.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/security/formsort_webhook_auth_field_lock.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js +1193 -0
- package/lib/esm/tests/api_tests/security/formsort_webhook_auth_field_lock.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/public_endpoint_auth_field_lock.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/security/public_endpoint_auth_field_lock.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/public_endpoint_auth_field_lock.test.js +2323 -0
- package/lib/esm/tests/api_tests/security/public_endpoint_auth_field_lock.test.js.map +1 -0
- package/lib/esm/tests/api_tests/tickets_bulk_assign_care_team.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/tickets_bulk_assign_care_team.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/tickets_bulk_assign_care_team.test.js +200 -0
- package/lib/esm/tests/api_tests/tickets_bulk_assign_care_team.test.js.map +1 -0
- package/lib/esm/tests/tests.d.ts.map +1 -1
- package/lib/esm/tests/tests.js +211 -178
- package/lib/esm/tests/tests.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +10 -10
- package/src/sdk.ts +4 -0
- package/src/tests/api_tests/auto_merge_form_submission.test.ts +13 -4
- package/src/tests/api_tests/healthie_multi_integration.test.ts +103 -5
- package/src/tests/api_tests/phone_calls_conference_hold.test.ts +62 -0
- package/src/tests/api_tests/scoped_api_keys.test.ts +5 -0
- package/src/tests/api_tests/security/F-0106-F-0110-enduser-write-restrictions.test.ts +38 -4
- package/src/tests/api_tests/security/F-0166-allowed-paths-wildcard-bypass.test.ts +506 -0
- package/src/tests/api_tests/security/enduser-ai-summary-trust-gate.test.ts +125 -0
- package/src/tests/api_tests/security/formsort_webhook_auth_field_lock.test.ts +753 -0
- package/src/tests/api_tests/security/public_endpoint_auth_field_lock.test.ts +1707 -0
- package/src/tests/api_tests/tickets_bulk_assign_care_team.test.ts +132 -0
- package/src/tests/tests.ts +20 -1
- package/test_generated.pdf +0 -0
- package/.env +0 -13
|
@@ -0,0 +1,1200 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __assign = (this && this.__assign) || function () {
|
|
3
|
+
__assign = Object.assign || function(t) {
|
|
4
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
5
|
+
s = arguments[i];
|
|
6
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
7
|
+
t[p] = s[p];
|
|
8
|
+
}
|
|
9
|
+
return t;
|
|
10
|
+
};
|
|
11
|
+
return __assign.apply(this, arguments);
|
|
12
|
+
};
|
|
13
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
14
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
15
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
16
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
17
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
18
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
19
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
23
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
|
24
|
+
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
25
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
26
|
+
function step(op) {
|
|
27
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
28
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
29
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
30
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
31
|
+
switch (op[0]) {
|
|
32
|
+
case 0: case 1: t = op; break;
|
|
33
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
34
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
35
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
36
|
+
default:
|
|
37
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
38
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
39
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
40
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
41
|
+
if (t[2]) _.ops.pop();
|
|
42
|
+
_.trys.pop(); continue;
|
|
43
|
+
}
|
|
44
|
+
op = body.call(thisArg, _);
|
|
45
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
46
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
50
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
51
|
+
};
|
|
52
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
53
|
+
exports.formsort_webhook_auth_field_lock_tests = void 0;
|
|
54
|
+
require('source-map-support').install();
|
|
55
|
+
var axios_1 = __importDefault(require("axios"));
|
|
56
|
+
var sdk_1 = require("../../../sdk");
|
|
57
|
+
var testing_1 = require("@tellescope/testing");
|
|
58
|
+
var setup_1 = require("../../setup");
|
|
59
|
+
var types_models_1 = require("@tellescope/types-models");
|
|
60
|
+
var host = process.env.API_URL || 'http://localhost:8080';
|
|
61
|
+
/**
|
|
62
|
+
* Regression test for: email / phone are locked against writes from the Formsort webhook when it
|
|
63
|
+
* IMPLICITLY matched a patient that already existed before the submission started.
|
|
64
|
+
*
|
|
65
|
+
* POST /v1/webhooks/formsort/* (alias: /v1/webhooks/form-ingestion/*)
|
|
66
|
+
*
|
|
67
|
+
* The handler resolves an enduser four ways, in precedence order:
|
|
68
|
+
* 1. ?enduserId= — an explicit assertion by the API-key-authenticated caller
|
|
69
|
+
* 2. existingResponse.enduserId — the patient attached to this responder_uuid's draft
|
|
70
|
+
* 3. findOne({ $or: [{ phone }, { email }] }) — a GUESS, optionally AND'ed with fname + lname
|
|
71
|
+
* 4. insert — brand-new patient
|
|
72
|
+
*
|
|
73
|
+
* On a finalized submission it then wrote a blanket `$set: enduserFields` onto whatever it landed
|
|
74
|
+
* on, and that object carries `email` and `phone`. Those are authentication channels:
|
|
75
|
+
* request-enduser-password-reset resolves purely by findOne({ email }), begin_login_flow accepts
|
|
76
|
+
* email OR phone, and send_otp delivers login codes to enduser.phone. So a submission that matched
|
|
77
|
+
* a patient on PHONE ALONE could repoint that patient's email (and vice versa), which chains to
|
|
78
|
+
* account takeover exactly as described in public_endpoint_auth_field_lock.test.ts.
|
|
79
|
+
*
|
|
80
|
+
* The rule under test: on path 3 — the guess — email and phone are dropped from the update. They
|
|
81
|
+
* remain writable on paths 1, 2 and 4.
|
|
82
|
+
*
|
|
83
|
+
* - Path 1 stays open deliberately. This endpoint is API-key authenticated, so a caller that
|
|
84
|
+
* names an enduserId knows which patient it means; keeping it writable preserves backwards
|
|
85
|
+
* compatibility and lets a sophisticated consumer update these fields when it is safe.
|
|
86
|
+
* - Path 2 stays open because Formsort fires a webhook per page: the patient who started the form
|
|
87
|
+
* must still be able to fix a typo'd email on page 3. It is gated on the STICKY
|
|
88
|
+
* FormResponse.authFieldIntakeDisabled flag, computed once when the response is first written,
|
|
89
|
+
* so a response first formed by a guess can never be re-opened by a later webhook.
|
|
90
|
+
*
|
|
91
|
+
* A "fill only if blank" fix would NOT close this. Phone-only patients are the common case
|
|
92
|
+
* (sms_messages.leave_message creates them), and filling their blank email is the primary attack —
|
|
93
|
+
* hence the UNSET victim shapes in R5.
|
|
94
|
+
*
|
|
95
|
+
* Scenario groups (pre-fix expectation in parens):
|
|
96
|
+
* R1 (RED) matched on phone — submitted email must not land
|
|
97
|
+
* R2 (RED) matched on email — submitted phone must not land
|
|
98
|
+
* R3 (RED) stickiness: draft matched a pre-existing patient, finalize must not repoint them
|
|
99
|
+
* R4 (RED) unresolvable ?enduserId= falls through to the guess — must stay blocked
|
|
100
|
+
* R5 (RED) victim shapes: blank email / blank phone are filled by the guess (the takeover case)
|
|
101
|
+
* G6 (GREEN) patient created by this responder's own draft may still correct their email
|
|
102
|
+
* G7 (GREEN) explicit ?enduserId= still writes email + phone (backwards-compat escape hatch)
|
|
103
|
+
* G8 (GREEN) ?createNewEnduser=true still writes email + phone
|
|
104
|
+
* G9 (GREEN) brand-new patient still inserted WITH submitted email + phone
|
|
105
|
+
* G10 (GREEN) fname/lname/dateOfBirth/gender/custom fields still update on a guessed match
|
|
106
|
+
* G11 (GREEN) explicit ?enduserId= overrides for that request only — the stored flag stays set
|
|
107
|
+
* G12 (GREEN) the Stripe grace: a BLANK phone on a contact the Stripe integration created within
|
|
108
|
+
* the last hour is filled from intake (see stripe_provisional_enduser.ts)
|
|
109
|
+
* R12 ...but a phone already stored on that contact is never overwritten
|
|
110
|
+
* R13 ...and its email is never repointed
|
|
111
|
+
* R14 both grace markers are load-bearing: neither source nor stripeCustomerId alone opens it
|
|
112
|
+
* R15 the grace is per-request — it does not clear the stored flag
|
|
113
|
+
*
|
|
114
|
+
* The Stripe grace is deliberately confined to THIS endpoint, which is API-key authenticated. The
|
|
115
|
+
* anonymous public-form path gets no exemption; that boundary is pinned by G28 in
|
|
116
|
+
* public_endpoint_auth_field_lock.test.ts.
|
|
117
|
+
*
|
|
118
|
+
* STRUCTURE: record -> report -> cleanup -> assert.
|
|
119
|
+
* EXIT_ON_FIRST_ERROR is true in testing.ts, so assert() calls process.exit(1) synchronously on the
|
|
120
|
+
* first failure. Two consequences shape this file:
|
|
121
|
+
* 1. No async_test here. Every scenario records into `results`, a summary table prints, and a
|
|
122
|
+
* single assert() loop runs LAST — so a pre-fix run prints the complete failure picture
|
|
123
|
+
* instead of dying on the first probe.
|
|
124
|
+
* 2. process.exit() does not unwind pending finally blocks, so cleanup runs BEFORE the assert
|
|
125
|
+
* loop; otherwise a by-design-failing run would leak every fixture into the next run.
|
|
126
|
+
*/
|
|
127
|
+
// The enduser update in this handler is fire-and-forget (.catch(console.error), not awaited).
|
|
128
|
+
// Reading the victim immediately would frequently show the un-overwritten value on a VULNERABLE
|
|
129
|
+
// server -> false pass. So poll for the attack LANDING: fails fast against vulnerable code, burns
|
|
130
|
+
// the full window against fixed code.
|
|
131
|
+
var OVERWRITE_WINDOW_MS = 3000;
|
|
132
|
+
var POLL_INTERVAL_MS = 150;
|
|
133
|
+
var sleep = function (ms) { return new Promise(function (resolve) { return setTimeout(resolve, ms); }); };
|
|
134
|
+
var formsort_webhook_auth_field_lock_tests = function (_a) {
|
|
135
|
+
var sdk = _a.sdk, sdkNonAdmin = _a.sdkNonAdmin;
|
|
136
|
+
return __awaiter(void 0, void 0, void 0, function () {
|
|
137
|
+
var RUN, results, record, skip, runScenario, phoneOffset, phoneSeed, nextPhone, emailFor, uuidFor, createdEnduserIds, createdFormIds, createdApiKeyIds, form, apiKey, postWebhook, makeVictim, pollEnduser, matched, stripeVictim, failures, skipped, _i, results_1, r, status_1, _b, createdFormIds_1, formId, responses, _c, responses_1, response, _d, createdFormIds_2, id, uniqueEnduserIds, _e, uniqueEnduserIds_1, id, _f, createdApiKeyIds_1, id, _g, results_2, r;
|
|
138
|
+
return __generator(this, function (_h) {
|
|
139
|
+
switch (_h.label) {
|
|
140
|
+
case 0:
|
|
141
|
+
(0, testing_1.log_header)("Formsort Webhook Auth Field Lock (email / phone)");
|
|
142
|
+
RUN = Date.now().toString(36);
|
|
143
|
+
results = [];
|
|
144
|
+
record = function (group, name, ok, detail) {
|
|
145
|
+
if (detail === void 0) { detail = ''; }
|
|
146
|
+
results.push({ group: group, name: name, ok: ok, detail: detail });
|
|
147
|
+
console.log(" ".concat(ok ? '✓' : '✗', " [").concat(group, "] ").concat(name).concat(detail ? " \u2014 ".concat(detail) : ''));
|
|
148
|
+
};
|
|
149
|
+
skip = function (group, name, detail) {
|
|
150
|
+
results.push({ group: group, name: name, ok: true, skipped: true, detail: detail });
|
|
151
|
+
console.log(" \u26A0 SKIP [".concat(group, "] ").concat(name, " \u2014 ").concat(detail));
|
|
152
|
+
};
|
|
153
|
+
runScenario = function (group, name, fn) { return __awaiter(void 0, void 0, void 0, function () {
|
|
154
|
+
var err_1, message;
|
|
155
|
+
var _a, _b;
|
|
156
|
+
return __generator(this, function (_c) {
|
|
157
|
+
switch (_c.label) {
|
|
158
|
+
case 0:
|
|
159
|
+
_c.trys.push([0, 2, , 3]);
|
|
160
|
+
return [4 /*yield*/, fn()];
|
|
161
|
+
case 1:
|
|
162
|
+
_c.sent();
|
|
163
|
+
return [3 /*break*/, 3];
|
|
164
|
+
case 2:
|
|
165
|
+
err_1 = _c.sent();
|
|
166
|
+
message = (_b = (_a = err_1 === null || err_1 === void 0 ? void 0 : err_1.message) !== null && _a !== void 0 ? _a : err_1 === null || err_1 === void 0 ? void 0 : err_1.error) !== null && _b !== void 0 ? _b : JSON.stringify(err_1 !== null && err_1 !== void 0 ? err_1 : {}).slice(0, 300);
|
|
167
|
+
record(group, name, false, "threw: ".concat(message));
|
|
168
|
+
return [3 /*break*/, 3];
|
|
169
|
+
case 3: return [2 /*return*/];
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}); };
|
|
173
|
+
phoneOffset = 0;
|
|
174
|
+
phoneSeed = Number(RUN.replace(/[^0-9]/g, '') || '0');
|
|
175
|
+
nextPhone = function () { return "+1555555".concat(String((phoneSeed + (phoneOffset += 1)) % 10000).padStart(4, '0')); };
|
|
176
|
+
emailFor = function (label) { return "fswh-".concat(label, "-").concat(RUN, "@tellescope.com").toLowerCase(); };
|
|
177
|
+
uuidFor = function (label) { return "fswh-".concat(label, "-").concat(RUN); };
|
|
178
|
+
createdEnduserIds = [];
|
|
179
|
+
createdFormIds = [];
|
|
180
|
+
createdApiKeyIds = [];
|
|
181
|
+
return [4 /*yield*/, sdk.api.forms.createOne({ title: "formsort auth field lock ".concat(RUN) })];
|
|
182
|
+
case 1:
|
|
183
|
+
form = _h.sent();
|
|
184
|
+
createdFormIds.push(form.id);
|
|
185
|
+
return [4 /*yield*/, sdk.api.api_keys.createOne({ scopes: [types_models_1.FORM_INGESTION_API_KEY_SCOPE] })];
|
|
186
|
+
case 2:
|
|
187
|
+
apiKey = _h.sent();
|
|
188
|
+
createdApiKeyIds.push(apiKey.id);
|
|
189
|
+
postWebhook = function (o) { return __awaiter(void 0, void 0, void 0, function () {
|
|
190
|
+
var url, res;
|
|
191
|
+
return __generator(this, function (_a) {
|
|
192
|
+
switch (_a.label) {
|
|
193
|
+
case 0:
|
|
194
|
+
url = new URL("".concat(host, "/v1/webhooks/form-ingestion/").concat(apiKey.key));
|
|
195
|
+
url.searchParams.set('formId', form.id);
|
|
196
|
+
url.searchParams.set('returnJSON', 'true');
|
|
197
|
+
if (o.enduserId)
|
|
198
|
+
url.searchParams.set('enduserId', o.enduserId);
|
|
199
|
+
if (o.createNewEnduser)
|
|
200
|
+
url.searchParams.set('createNewEnduser', 'true');
|
|
201
|
+
return [4 /*yield*/, axios_1.default.post(url.toString(), { answers: o.answers, responder_uuid: uuidFor(o.label), finalized: o.finalized }, { validateStatus: function () { return true; } })
|
|
202
|
+
// In-progress webhooks always return a bare 200 (the handler returns before the returnJSON
|
|
203
|
+
// block), so `data` is only populated for finalized calls.
|
|
204
|
+
];
|
|
205
|
+
case 1:
|
|
206
|
+
res = _a.sent();
|
|
207
|
+
// In-progress webhooks always return a bare 200 (the handler returns before the returnJSON
|
|
208
|
+
// block), so `data` is only populated for finalized calls.
|
|
209
|
+
return [2 /*return*/, { status: res.status, data: (res.data || {}) }];
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
}); };
|
|
213
|
+
makeVictim = function (label, fields) { return __awaiter(void 0, void 0, void 0, function () {
|
|
214
|
+
var victim;
|
|
215
|
+
return __generator(this, function (_a) {
|
|
216
|
+
switch (_a.label) {
|
|
217
|
+
case 0: return [4 /*yield*/, sdk.api.endusers.createOne(__assign({}, fields))];
|
|
218
|
+
case 1:
|
|
219
|
+
victim = _a.sent();
|
|
220
|
+
createdEnduserIds.push(victim.id);
|
|
221
|
+
return [2 /*return*/, victim];
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
}); };
|
|
225
|
+
pollEnduser = function (id, landed, windowMs) {
|
|
226
|
+
if (windowMs === void 0) { windowMs = OVERWRITE_WINDOW_MS; }
|
|
227
|
+
return __awaiter(void 0, void 0, void 0, function () {
|
|
228
|
+
var deadline, enduser;
|
|
229
|
+
return __generator(this, function (_a) {
|
|
230
|
+
switch (_a.label) {
|
|
231
|
+
case 0:
|
|
232
|
+
deadline = Date.now() + windowMs;
|
|
233
|
+
enduser = null;
|
|
234
|
+
_a.label = 1;
|
|
235
|
+
case 1:
|
|
236
|
+
if (!(Date.now() < deadline)) return [3 /*break*/, 4];
|
|
237
|
+
return [4 /*yield*/, sdk.api.endusers.getOne(id).catch(function () { return null; })];
|
|
238
|
+
case 2:
|
|
239
|
+
enduser = (_a.sent());
|
|
240
|
+
if (enduser && landed(enduser))
|
|
241
|
+
return [2 /*return*/, enduser];
|
|
242
|
+
return [4 /*yield*/, sleep(POLL_INTERVAL_MS)];
|
|
243
|
+
case 3:
|
|
244
|
+
_a.sent();
|
|
245
|
+
return [3 /*break*/, 1];
|
|
246
|
+
case 4: return [2 /*return*/, enduser];
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
};
|
|
251
|
+
matched = function (group, name, data, victimId) {
|
|
252
|
+
if ((data === null || data === void 0 ? void 0 : data.enduserId) === victimId)
|
|
253
|
+
return true;
|
|
254
|
+
record(group, name, false, "probe invalid: webhook landed on enduserId=".concat((data === null || data === void 0 ? void 0 : data.enduserId) || '(none)', " expected victim=").concat(victimId));
|
|
255
|
+
return false;
|
|
256
|
+
};
|
|
257
|
+
// ---------------------------------------------------------------------------------------------
|
|
258
|
+
// R1 — matched on PHONE, submitted email must not land.
|
|
259
|
+
// ---------------------------------------------------------------------------------------------
|
|
260
|
+
return [4 /*yield*/, runScenario('R1', 'phone match does not repoint email', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
261
|
+
var originalEmail, phone, victim, attackerEmail, data, enduser;
|
|
262
|
+
return __generator(this, function (_a) {
|
|
263
|
+
switch (_a.label) {
|
|
264
|
+
case 0:
|
|
265
|
+
originalEmail = emailFor('r1');
|
|
266
|
+
phone = nextPhone();
|
|
267
|
+
return [4 /*yield*/, makeVictim('r1', { email: originalEmail, phone: phone })];
|
|
268
|
+
case 1:
|
|
269
|
+
victim = _a.sent();
|
|
270
|
+
attackerEmail = emailFor('r1-attacker');
|
|
271
|
+
return [4 /*yield*/, postWebhook({
|
|
272
|
+
label: 'r1',
|
|
273
|
+
finalized: true,
|
|
274
|
+
answers: [{ key: 'phone', value: phone }, { key: 'email', value: attackerEmail }],
|
|
275
|
+
})];
|
|
276
|
+
case 2:
|
|
277
|
+
data = (_a.sent()).data;
|
|
278
|
+
if (!matched('R1', 'phone match does not repoint email', data, victim.id))
|
|
279
|
+
return [2 /*return*/];
|
|
280
|
+
return [4 /*yield*/, pollEnduser(victim.id, function (e) { return e.email === attackerEmail; })];
|
|
281
|
+
case 3:
|
|
282
|
+
enduser = _a.sent();
|
|
283
|
+
record('R1', 'phone match does not repoint email', (enduser === null || enduser === void 0 ? void 0 : enduser.email) === originalEmail, "email=".concat((enduser === null || enduser === void 0 ? void 0 : enduser.email) || '(unset)', " expected=").concat(originalEmail));
|
|
284
|
+
return [2 /*return*/];
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
}); })
|
|
288
|
+
// ---------------------------------------------------------------------------------------------
|
|
289
|
+
// R2 — matched on EMAIL, submitted phone must not land.
|
|
290
|
+
// ---------------------------------------------------------------------------------------------
|
|
291
|
+
];
|
|
292
|
+
case 3:
|
|
293
|
+
// ---------------------------------------------------------------------------------------------
|
|
294
|
+
// R1 — matched on PHONE, submitted email must not land.
|
|
295
|
+
// ---------------------------------------------------------------------------------------------
|
|
296
|
+
_h.sent();
|
|
297
|
+
// ---------------------------------------------------------------------------------------------
|
|
298
|
+
// R2 — matched on EMAIL, submitted phone must not land.
|
|
299
|
+
// ---------------------------------------------------------------------------------------------
|
|
300
|
+
return [4 /*yield*/, runScenario('R2', 'email match does not repoint phone', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
301
|
+
var email, originalPhone, victim, attackerPhone, data, enduser;
|
|
302
|
+
return __generator(this, function (_a) {
|
|
303
|
+
switch (_a.label) {
|
|
304
|
+
case 0:
|
|
305
|
+
email = emailFor('r2');
|
|
306
|
+
originalPhone = nextPhone();
|
|
307
|
+
return [4 /*yield*/, makeVictim('r2', { email: email, phone: originalPhone })];
|
|
308
|
+
case 1:
|
|
309
|
+
victim = _a.sent();
|
|
310
|
+
attackerPhone = nextPhone();
|
|
311
|
+
return [4 /*yield*/, postWebhook({
|
|
312
|
+
label: 'r2',
|
|
313
|
+
finalized: true,
|
|
314
|
+
answers: [{ key: 'email', value: email }, { key: 'phone', value: attackerPhone }],
|
|
315
|
+
})];
|
|
316
|
+
case 2:
|
|
317
|
+
data = (_a.sent()).data;
|
|
318
|
+
if (!matched('R2', 'email match does not repoint phone', data, victim.id))
|
|
319
|
+
return [2 /*return*/];
|
|
320
|
+
return [4 /*yield*/, pollEnduser(victim.id, function (e) { return e.phone === attackerPhone; })];
|
|
321
|
+
case 3:
|
|
322
|
+
enduser = _a.sent();
|
|
323
|
+
record('R2', 'email match does not repoint phone', (enduser === null || enduser === void 0 ? void 0 : enduser.phone) === originalPhone, "phone=".concat((enduser === null || enduser === void 0 ? void 0 : enduser.phone) || '(unset)', " expected=").concat(originalPhone));
|
|
324
|
+
return [2 /*return*/];
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
}); })
|
|
328
|
+
// ---------------------------------------------------------------------------------------------
|
|
329
|
+
// R3 — stickiness. The draft matched a pre-existing patient by phone; the finalize resolves that
|
|
330
|
+
// patient via existingResponse.enduserId (path 2), which is the OPEN path. It must stay blocked,
|
|
331
|
+
// because the response was formed by a guess — that is what the sticky flag records.
|
|
332
|
+
// ---------------------------------------------------------------------------------------------
|
|
333
|
+
];
|
|
334
|
+
case 4:
|
|
335
|
+
// ---------------------------------------------------------------------------------------------
|
|
336
|
+
// R2 — matched on EMAIL, submitted phone must not land.
|
|
337
|
+
// ---------------------------------------------------------------------------------------------
|
|
338
|
+
_h.sent();
|
|
339
|
+
// ---------------------------------------------------------------------------------------------
|
|
340
|
+
// R3 — stickiness. The draft matched a pre-existing patient by phone; the finalize resolves that
|
|
341
|
+
// patient via existingResponse.enduserId (path 2), which is the OPEN path. It must stay blocked,
|
|
342
|
+
// because the response was formed by a guess — that is what the sticky flag records.
|
|
343
|
+
// ---------------------------------------------------------------------------------------------
|
|
344
|
+
return [4 /*yield*/, runScenario('R3', 'draft formed by a guess stays blocked on finalize', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
345
|
+
var originalEmail, phone, victim, attackerEmail, data, enduser;
|
|
346
|
+
return __generator(this, function (_a) {
|
|
347
|
+
switch (_a.label) {
|
|
348
|
+
case 0:
|
|
349
|
+
originalEmail = emailFor('r3');
|
|
350
|
+
phone = nextPhone();
|
|
351
|
+
return [4 /*yield*/, makeVictim('r3', { email: originalEmail, phone: phone })];
|
|
352
|
+
case 1:
|
|
353
|
+
victim = _a.sent();
|
|
354
|
+
attackerEmail = emailFor('r3-attacker');
|
|
355
|
+
// in-progress webhook: enough to match (phone present), creates the draft response
|
|
356
|
+
return [4 /*yield*/, postWebhook({ label: 'r3', finalized: false, answers: [{ key: 'phone', value: phone }] })];
|
|
357
|
+
case 2:
|
|
358
|
+
// in-progress webhook: enough to match (phone present), creates the draft response
|
|
359
|
+
_a.sent();
|
|
360
|
+
return [4 /*yield*/, postWebhook({
|
|
361
|
+
label: 'r3',
|
|
362
|
+
finalized: true,
|
|
363
|
+
answers: [{ key: 'phone', value: phone }, { key: 'email', value: attackerEmail }],
|
|
364
|
+
})];
|
|
365
|
+
case 3:
|
|
366
|
+
data = (_a.sent()).data;
|
|
367
|
+
if (!matched('R3', 'draft formed by a guess stays blocked on finalize', data, victim.id))
|
|
368
|
+
return [2 /*return*/];
|
|
369
|
+
return [4 /*yield*/, pollEnduser(victim.id, function (e) { return e.email === attackerEmail; })];
|
|
370
|
+
case 4:
|
|
371
|
+
enduser = _a.sent();
|
|
372
|
+
record('R3', 'draft formed by a guess stays blocked on finalize', (enduser === null || enduser === void 0 ? void 0 : enduser.email) === originalEmail, "email=".concat((enduser === null || enduser === void 0 ? void 0 : enduser.email) || '(unset)', " expected=").concat(originalEmail));
|
|
373
|
+
return [2 /*return*/];
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
}); })
|
|
377
|
+
// ---------------------------------------------------------------------------------------------
|
|
378
|
+
// R4 — an unresolvable ?enduserId= must NOT count as an explicit assertion. findById returns null,
|
|
379
|
+
// the chain falls through to the $or guess, and the write must stay blocked. This pins that the
|
|
380
|
+
// override keys off the RESOLVED record, not the mere presence of the query param. A cross-tenant
|
|
381
|
+
// id takes the same route: DB is businessId-scoped, so findById returns null for it too.
|
|
382
|
+
// ---------------------------------------------------------------------------------------------
|
|
383
|
+
];
|
|
384
|
+
case 5:
|
|
385
|
+
// ---------------------------------------------------------------------------------------------
|
|
386
|
+
// R3 — stickiness. The draft matched a pre-existing patient by phone; the finalize resolves that
|
|
387
|
+
// patient via existingResponse.enduserId (path 2), which is the OPEN path. It must stay blocked,
|
|
388
|
+
// because the response was formed by a guess — that is what the sticky flag records.
|
|
389
|
+
// ---------------------------------------------------------------------------------------------
|
|
390
|
+
_h.sent();
|
|
391
|
+
// ---------------------------------------------------------------------------------------------
|
|
392
|
+
// R4 — an unresolvable ?enduserId= must NOT count as an explicit assertion. findById returns null,
|
|
393
|
+
// the chain falls through to the $or guess, and the write must stay blocked. This pins that the
|
|
394
|
+
// override keys off the RESOLVED record, not the mere presence of the query param. A cross-tenant
|
|
395
|
+
// id takes the same route: DB is businessId-scoped, so findById returns null for it too.
|
|
396
|
+
// ---------------------------------------------------------------------------------------------
|
|
397
|
+
return [4 /*yield*/, runScenario('R4', 'unresolvable enduserId falls through to the guess', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
398
|
+
var originalEmail, phone, victim, attackerEmail, data, enduser;
|
|
399
|
+
return __generator(this, function (_a) {
|
|
400
|
+
switch (_a.label) {
|
|
401
|
+
case 0:
|
|
402
|
+
originalEmail = emailFor('r4');
|
|
403
|
+
phone = nextPhone();
|
|
404
|
+
return [4 /*yield*/, makeVictim('r4', { email: originalEmail, phone: phone })];
|
|
405
|
+
case 1:
|
|
406
|
+
victim = _a.sent();
|
|
407
|
+
attackerEmail = emailFor('r4-attacker');
|
|
408
|
+
return [4 /*yield*/, postWebhook({
|
|
409
|
+
label: 'r4',
|
|
410
|
+
finalized: true,
|
|
411
|
+
enduserId: '0000000000000000000000ff',
|
|
412
|
+
answers: [{ key: 'phone', value: phone }, { key: 'email', value: attackerEmail }],
|
|
413
|
+
})];
|
|
414
|
+
case 2:
|
|
415
|
+
data = (_a.sent()).data;
|
|
416
|
+
if (!matched('R4', 'unresolvable enduserId falls through to the guess', data, victim.id))
|
|
417
|
+
return [2 /*return*/];
|
|
418
|
+
return [4 /*yield*/, pollEnduser(victim.id, function (e) { return e.email === attackerEmail; })];
|
|
419
|
+
case 3:
|
|
420
|
+
enduser = _a.sent();
|
|
421
|
+
record('R4', 'unresolvable enduserId falls through to the guess', (enduser === null || enduser === void 0 ? void 0 : enduser.email) === originalEmail, "email=".concat((enduser === null || enduser === void 0 ? void 0 : enduser.email) || '(unset)', " expected=").concat(originalEmail));
|
|
422
|
+
return [2 /*return*/];
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
}); })
|
|
426
|
+
// ---------------------------------------------------------------------------------------------
|
|
427
|
+
// R5 — victim shapes. A blank field is the LOAD-BEARING case: phone-only patients are common and
|
|
428
|
+
// cheap to create, and filling their blank email is the takeover primitive. "Only if blank" is
|
|
429
|
+
// therefore the wrong fix, and these two rows are what prove it.
|
|
430
|
+
// ---------------------------------------------------------------------------------------------
|
|
431
|
+
];
|
|
432
|
+
case 6:
|
|
433
|
+
// ---------------------------------------------------------------------------------------------
|
|
434
|
+
// R4 — an unresolvable ?enduserId= must NOT count as an explicit assertion. findById returns null,
|
|
435
|
+
// the chain falls through to the $or guess, and the write must stay blocked. This pins that the
|
|
436
|
+
// override keys off the RESOLVED record, not the mere presence of the query param. A cross-tenant
|
|
437
|
+
// id takes the same route: DB is businessId-scoped, so findById returns null for it too.
|
|
438
|
+
// ---------------------------------------------------------------------------------------------
|
|
439
|
+
_h.sent();
|
|
440
|
+
// ---------------------------------------------------------------------------------------------
|
|
441
|
+
// R5 — victim shapes. A blank field is the LOAD-BEARING case: phone-only patients are common and
|
|
442
|
+
// cheap to create, and filling their blank email is the takeover primitive. "Only if blank" is
|
|
443
|
+
// therefore the wrong fix, and these two rows are what prove it.
|
|
444
|
+
// ---------------------------------------------------------------------------------------------
|
|
445
|
+
return [4 /*yield*/, runScenario('R5', 'blank email on a phone-only patient is not filled by a guess', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
446
|
+
var phone, victim, attackerEmail, data, enduser;
|
|
447
|
+
return __generator(this, function (_a) {
|
|
448
|
+
switch (_a.label) {
|
|
449
|
+
case 0:
|
|
450
|
+
phone = nextPhone();
|
|
451
|
+
return [4 /*yield*/, makeVictim('r5a', { phone: phone, fname: 'Blank', lname: "Email".concat(RUN) })];
|
|
452
|
+
case 1:
|
|
453
|
+
victim = _a.sent();
|
|
454
|
+
attackerEmail = emailFor('r5a-attacker');
|
|
455
|
+
return [4 /*yield*/, postWebhook({
|
|
456
|
+
label: 'r5a',
|
|
457
|
+
finalized: true,
|
|
458
|
+
answers: [{ key: 'phone', value: phone }, { key: 'email', value: attackerEmail }],
|
|
459
|
+
})];
|
|
460
|
+
case 2:
|
|
461
|
+
data = (_a.sent()).data;
|
|
462
|
+
if (!matched('R5', 'blank email on a phone-only patient is not filled by a guess', data, victim.id))
|
|
463
|
+
return [2 /*return*/];
|
|
464
|
+
return [4 /*yield*/, pollEnduser(victim.id, function (e) { return e.email === attackerEmail; })];
|
|
465
|
+
case 3:
|
|
466
|
+
enduser = _a.sent();
|
|
467
|
+
record('R5', 'blank email on a phone-only patient is not filled by a guess', !(enduser === null || enduser === void 0 ? void 0 : enduser.email), "email=".concat((enduser === null || enduser === void 0 ? void 0 : enduser.email) || '(unset)', " expected=(unset)"));
|
|
468
|
+
return [2 /*return*/];
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
}); })];
|
|
472
|
+
case 7:
|
|
473
|
+
// ---------------------------------------------------------------------------------------------
|
|
474
|
+
// R5 — victim shapes. A blank field is the LOAD-BEARING case: phone-only patients are common and
|
|
475
|
+
// cheap to create, and filling their blank email is the takeover primitive. "Only if blank" is
|
|
476
|
+
// therefore the wrong fix, and these two rows are what prove it.
|
|
477
|
+
// ---------------------------------------------------------------------------------------------
|
|
478
|
+
_h.sent();
|
|
479
|
+
return [4 /*yield*/, runScenario('R5', 'blank phone on an email-only patient is not filled by a guess', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
480
|
+
var email, victim, attackerPhone, data, enduser;
|
|
481
|
+
return __generator(this, function (_a) {
|
|
482
|
+
switch (_a.label) {
|
|
483
|
+
case 0:
|
|
484
|
+
email = emailFor('r5b');
|
|
485
|
+
return [4 /*yield*/, makeVictim('r5b', { email: email })];
|
|
486
|
+
case 1:
|
|
487
|
+
victim = _a.sent();
|
|
488
|
+
attackerPhone = nextPhone();
|
|
489
|
+
return [4 /*yield*/, postWebhook({
|
|
490
|
+
label: 'r5b',
|
|
491
|
+
finalized: true,
|
|
492
|
+
answers: [{ key: 'email', value: email }, { key: 'phone', value: attackerPhone }],
|
|
493
|
+
})];
|
|
494
|
+
case 2:
|
|
495
|
+
data = (_a.sent()).data;
|
|
496
|
+
if (!matched('R5', 'blank phone on an email-only patient is not filled by a guess', data, victim.id))
|
|
497
|
+
return [2 /*return*/];
|
|
498
|
+
return [4 /*yield*/, pollEnduser(victim.id, function (e) { return e.phone === attackerPhone; })];
|
|
499
|
+
case 3:
|
|
500
|
+
enduser = _a.sent();
|
|
501
|
+
record('R5', 'blank phone on an email-only patient is not filled by a guess', !(enduser === null || enduser === void 0 ? void 0 : enduser.phone), "phone=".concat((enduser === null || enduser === void 0 ? void 0 : enduser.phone) || '(unset)', " expected=(unset)"));
|
|
502
|
+
return [2 /*return*/];
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
}); })
|
|
506
|
+
// ---------------------------------------------------------------------------------------------
|
|
507
|
+
// G6 — the carve-out. The draft CREATED this patient (no prior record matched), so the response is
|
|
508
|
+
// not flagged and the same person may still correct their email on a later page of the form.
|
|
509
|
+
// ---------------------------------------------------------------------------------------------
|
|
510
|
+
];
|
|
511
|
+
case 8:
|
|
512
|
+
_h.sent();
|
|
513
|
+
// ---------------------------------------------------------------------------------------------
|
|
514
|
+
// G6 — the carve-out. The draft CREATED this patient (no prior record matched), so the response is
|
|
515
|
+
// not flagged and the same person may still correct their email on a later page of the form.
|
|
516
|
+
// ---------------------------------------------------------------------------------------------
|
|
517
|
+
return [4 /*yield*/, runScenario('G6', 'patient created by this draft may still correct their email', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
518
|
+
var firstEmail, correctedEmail, phone, data, enduser;
|
|
519
|
+
return __generator(this, function (_a) {
|
|
520
|
+
switch (_a.label) {
|
|
521
|
+
case 0:
|
|
522
|
+
firstEmail = emailFor('g6-typo');
|
|
523
|
+
correctedEmail = emailFor('g6-correct');
|
|
524
|
+
phone = nextPhone();
|
|
525
|
+
// in-progress webhook with a never-before-seen email: inserts a new patient, response unflagged
|
|
526
|
+
return [4 /*yield*/, postWebhook({
|
|
527
|
+
label: 'g6',
|
|
528
|
+
finalized: false,
|
|
529
|
+
answers: [{ key: 'email', value: firstEmail }, { key: 'phone', value: phone }],
|
|
530
|
+
})];
|
|
531
|
+
case 1:
|
|
532
|
+
// in-progress webhook with a never-before-seen email: inserts a new patient, response unflagged
|
|
533
|
+
_a.sent();
|
|
534
|
+
return [4 /*yield*/, postWebhook({
|
|
535
|
+
label: 'g6',
|
|
536
|
+
finalized: true,
|
|
537
|
+
answers: [{ key: 'email', value: correctedEmail }, { key: 'phone', value: phone }],
|
|
538
|
+
})];
|
|
539
|
+
case 2:
|
|
540
|
+
data = (_a.sent()).data;
|
|
541
|
+
if (!(data === null || data === void 0 ? void 0 : data.enduserId)) {
|
|
542
|
+
record('G6', 'patient created by this draft may still correct their email', false, 'probe invalid: webhook returned no enduserId');
|
|
543
|
+
return [2 /*return*/];
|
|
544
|
+
}
|
|
545
|
+
createdEnduserIds.push(data.enduserId);
|
|
546
|
+
return [4 /*yield*/, pollEnduser(data.enduserId, function (e) { return e.email === correctedEmail; })];
|
|
547
|
+
case 3:
|
|
548
|
+
enduser = _a.sent();
|
|
549
|
+
record('G6', 'patient created by this draft may still correct their email', (enduser === null || enduser === void 0 ? void 0 : enduser.email) === correctedEmail, "email=".concat((enduser === null || enduser === void 0 ? void 0 : enduser.email) || '(unset)', " expected=").concat(correctedEmail));
|
|
550
|
+
return [2 /*return*/];
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
}); })
|
|
554
|
+
// ---------------------------------------------------------------------------------------------
|
|
555
|
+
// G7 — the backwards-compat escape hatch. An API-key-authenticated caller that names the patient
|
|
556
|
+
// explicitly still writes email and phone. Pinned so a later tightening can't silently remove it.
|
|
557
|
+
// ---------------------------------------------------------------------------------------------
|
|
558
|
+
];
|
|
559
|
+
case 9:
|
|
560
|
+
// ---------------------------------------------------------------------------------------------
|
|
561
|
+
// G6 — the carve-out. The draft CREATED this patient (no prior record matched), so the response is
|
|
562
|
+
// not flagged and the same person may still correct their email on a later page of the form.
|
|
563
|
+
// ---------------------------------------------------------------------------------------------
|
|
564
|
+
_h.sent();
|
|
565
|
+
// ---------------------------------------------------------------------------------------------
|
|
566
|
+
// G7 — the backwards-compat escape hatch. An API-key-authenticated caller that names the patient
|
|
567
|
+
// explicitly still writes email and phone. Pinned so a later tightening can't silently remove it.
|
|
568
|
+
// ---------------------------------------------------------------------------------------------
|
|
569
|
+
return [4 /*yield*/, runScenario('G7', 'explicit enduserId still writes email and phone', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
570
|
+
var victim, newEmail, newPhone, data, enduser;
|
|
571
|
+
return __generator(this, function (_a) {
|
|
572
|
+
switch (_a.label) {
|
|
573
|
+
case 0: return [4 /*yield*/, makeVictim('g7', { email: emailFor('g7'), phone: nextPhone() })];
|
|
574
|
+
case 1:
|
|
575
|
+
victim = _a.sent();
|
|
576
|
+
newEmail = emailFor('g7-new');
|
|
577
|
+
newPhone = nextPhone();
|
|
578
|
+
return [4 /*yield*/, postWebhook({
|
|
579
|
+
label: 'g7',
|
|
580
|
+
finalized: true,
|
|
581
|
+
enduserId: victim.id,
|
|
582
|
+
answers: [{ key: 'email', value: newEmail }, { key: 'phone', value: newPhone }],
|
|
583
|
+
})];
|
|
584
|
+
case 2:
|
|
585
|
+
data = (_a.sent()).data;
|
|
586
|
+
if (!matched('G7', 'explicit enduserId still writes email and phone', data, victim.id))
|
|
587
|
+
return [2 /*return*/];
|
|
588
|
+
return [4 /*yield*/, pollEnduser(victim.id, function (e) { return e.email === newEmail && e.phone === newPhone; })];
|
|
589
|
+
case 3:
|
|
590
|
+
enduser = _a.sent();
|
|
591
|
+
record('G7', 'explicit enduserId still writes email', (enduser === null || enduser === void 0 ? void 0 : enduser.email) === newEmail, "email=".concat((enduser === null || enduser === void 0 ? void 0 : enduser.email) || '(unset)', " expected=").concat(newEmail));
|
|
592
|
+
record('G7', 'explicit enduserId still writes phone', (enduser === null || enduser === void 0 ? void 0 : enduser.phone) === newPhone, "phone=".concat((enduser === null || enduser === void 0 ? void 0 : enduser.phone) || '(unset)', " expected=").concat(newPhone));
|
|
593
|
+
return [2 /*return*/];
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
}); })
|
|
597
|
+
// ---------------------------------------------------------------------------------------------
|
|
598
|
+
// G8 — ?createNewEnduser=true inserts a BLANK record up front and then fills it via the update
|
|
599
|
+
// branch, so email/phone must still land there.
|
|
600
|
+
// ---------------------------------------------------------------------------------------------
|
|
601
|
+
];
|
|
602
|
+
case 10:
|
|
603
|
+
// ---------------------------------------------------------------------------------------------
|
|
604
|
+
// G7 — the backwards-compat escape hatch. An API-key-authenticated caller that names the patient
|
|
605
|
+
// explicitly still writes email and phone. Pinned so a later tightening can't silently remove it.
|
|
606
|
+
// ---------------------------------------------------------------------------------------------
|
|
607
|
+
_h.sent();
|
|
608
|
+
// ---------------------------------------------------------------------------------------------
|
|
609
|
+
// G8 — ?createNewEnduser=true inserts a BLANK record up front and then fills it via the update
|
|
610
|
+
// branch, so email/phone must still land there.
|
|
611
|
+
// ---------------------------------------------------------------------------------------------
|
|
612
|
+
return [4 /*yield*/, runScenario('G8', 'createNewEnduser still writes email and phone', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
613
|
+
var newEmail, newPhone, data, enduser;
|
|
614
|
+
return __generator(this, function (_a) {
|
|
615
|
+
switch (_a.label) {
|
|
616
|
+
case 0:
|
|
617
|
+
newEmail = emailFor('g8');
|
|
618
|
+
newPhone = nextPhone();
|
|
619
|
+
return [4 /*yield*/, postWebhook({
|
|
620
|
+
label: 'g8',
|
|
621
|
+
finalized: true,
|
|
622
|
+
createNewEnduser: true,
|
|
623
|
+
answers: [{ key: 'email', value: newEmail }, { key: 'phone', value: newPhone }],
|
|
624
|
+
})];
|
|
625
|
+
case 1:
|
|
626
|
+
data = (_a.sent()).data;
|
|
627
|
+
if (!(data === null || data === void 0 ? void 0 : data.enduserId)) {
|
|
628
|
+
record('G8', 'createNewEnduser still writes email and phone', false, 'probe invalid: webhook returned no enduserId');
|
|
629
|
+
return [2 /*return*/];
|
|
630
|
+
}
|
|
631
|
+
createdEnduserIds.push(data.enduserId);
|
|
632
|
+
return [4 /*yield*/, pollEnduser(data.enduserId, function (e) { return e.email === newEmail && e.phone === newPhone; })];
|
|
633
|
+
case 2:
|
|
634
|
+
enduser = _a.sent();
|
|
635
|
+
record('G8', 'createNewEnduser still writes email', (enduser === null || enduser === void 0 ? void 0 : enduser.email) === newEmail, "email=".concat((enduser === null || enduser === void 0 ? void 0 : enduser.email) || '(unset)', " expected=").concat(newEmail));
|
|
636
|
+
record('G8', 'createNewEnduser still writes phone', (enduser === null || enduser === void 0 ? void 0 : enduser.phone) === newPhone, "phone=".concat((enduser === null || enduser === void 0 ? void 0 : enduser.phone) || '(unset)', " expected=").concat(newPhone));
|
|
637
|
+
return [2 /*return*/];
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
}); })
|
|
641
|
+
// ---------------------------------------------------------------------------------------------
|
|
642
|
+
// G9 — brand-new patient, no prior record to match: the INSERT path keeps both fields.
|
|
643
|
+
// ---------------------------------------------------------------------------------------------
|
|
644
|
+
];
|
|
645
|
+
case 11:
|
|
646
|
+
// ---------------------------------------------------------------------------------------------
|
|
647
|
+
// G8 — ?createNewEnduser=true inserts a BLANK record up front and then fills it via the update
|
|
648
|
+
// branch, so email/phone must still land there.
|
|
649
|
+
// ---------------------------------------------------------------------------------------------
|
|
650
|
+
_h.sent();
|
|
651
|
+
// ---------------------------------------------------------------------------------------------
|
|
652
|
+
// G9 — brand-new patient, no prior record to match: the INSERT path keeps both fields.
|
|
653
|
+
// ---------------------------------------------------------------------------------------------
|
|
654
|
+
return [4 /*yield*/, runScenario('G9', 'brand-new patient is inserted with email and phone', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
655
|
+
var newEmail, newPhone, data, enduser;
|
|
656
|
+
return __generator(this, function (_a) {
|
|
657
|
+
switch (_a.label) {
|
|
658
|
+
case 0:
|
|
659
|
+
newEmail = emailFor('g9');
|
|
660
|
+
newPhone = nextPhone();
|
|
661
|
+
return [4 /*yield*/, postWebhook({
|
|
662
|
+
label: 'g9',
|
|
663
|
+
finalized: true,
|
|
664
|
+
answers: [{ key: 'email', value: newEmail }, { key: 'phone', value: newPhone }],
|
|
665
|
+
})];
|
|
666
|
+
case 1:
|
|
667
|
+
data = (_a.sent()).data;
|
|
668
|
+
if (!(data === null || data === void 0 ? void 0 : data.enduserId)) {
|
|
669
|
+
record('G9', 'brand-new patient is inserted with email and phone', false, 'probe invalid: webhook returned no enduserId');
|
|
670
|
+
return [2 /*return*/];
|
|
671
|
+
}
|
|
672
|
+
createdEnduserIds.push(data.enduserId);
|
|
673
|
+
return [4 /*yield*/, pollEnduser(data.enduserId, function (e) { return !!e.email && !!e.phone; })];
|
|
674
|
+
case 2:
|
|
675
|
+
enduser = _a.sent();
|
|
676
|
+
record('G9', 'brand-new patient is inserted with email', (enduser === null || enduser === void 0 ? void 0 : enduser.email) === newEmail, "email=".concat((enduser === null || enduser === void 0 ? void 0 : enduser.email) || '(unset)', " expected=").concat(newEmail));
|
|
677
|
+
record('G9', 'brand-new patient is inserted with phone', (enduser === null || enduser === void 0 ? void 0 : enduser.phone) === newPhone, "phone=".concat((enduser === null || enduser === void 0 ? void 0 : enduser.phone) || '(unset)', " expected=").concat(newPhone));
|
|
678
|
+
return [2 /*return*/];
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
}); })
|
|
682
|
+
// ---------------------------------------------------------------------------------------------
|
|
683
|
+
// G10 — the narrowing is email/phone ONLY. Everything else the webhook parses must still update
|
|
684
|
+
// on a guessed match, otherwise this fix has quietly broken Formsort intake.
|
|
685
|
+
// ---------------------------------------------------------------------------------------------
|
|
686
|
+
];
|
|
687
|
+
case 12:
|
|
688
|
+
// ---------------------------------------------------------------------------------------------
|
|
689
|
+
// G9 — brand-new patient, no prior record to match: the INSERT path keeps both fields.
|
|
690
|
+
// ---------------------------------------------------------------------------------------------
|
|
691
|
+
_h.sent();
|
|
692
|
+
// ---------------------------------------------------------------------------------------------
|
|
693
|
+
// G10 — the narrowing is email/phone ONLY. Everything else the webhook parses must still update
|
|
694
|
+
// on a guessed match, otherwise this fix has quietly broken Formsort intake.
|
|
695
|
+
// ---------------------------------------------------------------------------------------------
|
|
696
|
+
return [4 /*yield*/, runScenario('G10', 'non-auth fields still update on a guessed match', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
697
|
+
var originalEmail, phone, victim, data, enduser;
|
|
698
|
+
var _a, _b;
|
|
699
|
+
return __generator(this, function (_c) {
|
|
700
|
+
switch (_c.label) {
|
|
701
|
+
case 0:
|
|
702
|
+
originalEmail = emailFor('g10');
|
|
703
|
+
phone = nextPhone();
|
|
704
|
+
return [4 /*yield*/, makeVictim('g10', {
|
|
705
|
+
email: originalEmail,
|
|
706
|
+
phone: phone,
|
|
707
|
+
fname: 'Before', lname: 'Before',
|
|
708
|
+
})];
|
|
709
|
+
case 1:
|
|
710
|
+
victim = _c.sent();
|
|
711
|
+
return [4 /*yield*/, postWebhook({
|
|
712
|
+
label: 'g10',
|
|
713
|
+
finalized: true,
|
|
714
|
+
answers: [
|
|
715
|
+
{ key: 'phone', value: phone },
|
|
716
|
+
{ key: 'email', value: emailFor('g10-attacker') },
|
|
717
|
+
{ key: 'fname', value: 'After' },
|
|
718
|
+
{ key: 'lname', value: 'After' },
|
|
719
|
+
{ key: 'gender', value: 'Male' },
|
|
720
|
+
{ key: 'dateOfBirth', value: '1990-01-02' },
|
|
721
|
+
{ key: 'ts_enduser_lockprobe', value: "probe-".concat(RUN) },
|
|
722
|
+
],
|
|
723
|
+
})];
|
|
724
|
+
case 2:
|
|
725
|
+
data = (_c.sent()).data;
|
|
726
|
+
if (!matched('G10', 'non-auth fields still update on a guessed match', data, victim.id))
|
|
727
|
+
return [2 /*return*/];
|
|
728
|
+
return [4 /*yield*/, pollEnduser(victim.id, function (e) { return e.fname === 'After'; })];
|
|
729
|
+
case 3:
|
|
730
|
+
enduser = _c.sent();
|
|
731
|
+
record('G10', 'fname still updates', (enduser === null || enduser === void 0 ? void 0 : enduser.fname) === 'After', "fname=".concat((enduser === null || enduser === void 0 ? void 0 : enduser.fname) || '(unset)', " expected=After"));
|
|
732
|
+
record('G10', 'lname still updates', (enduser === null || enduser === void 0 ? void 0 : enduser.lname) === 'After', "lname=".concat((enduser === null || enduser === void 0 ? void 0 : enduser.lname) || '(unset)', " expected=After"));
|
|
733
|
+
record('G10', 'gender still updates', (enduser === null || enduser === void 0 ? void 0 : enduser.gender) === 'Male', "gender=".concat((enduser === null || enduser === void 0 ? void 0 : enduser.gender) || '(unset)', " expected=Male"));
|
|
734
|
+
record('G10', 'dateOfBirth still updates', (enduser === null || enduser === void 0 ? void 0 : enduser.dateOfBirth) === '01-02-1990', "dateOfBirth=".concat((enduser === null || enduser === void 0 ? void 0 : enduser.dateOfBirth) || '(unset)', " expected=01-02-1990"));
|
|
735
|
+
record('G10', 'custom fields still update', ((_a = enduser === null || enduser === void 0 ? void 0 : enduser.fields) === null || _a === void 0 ? void 0 : _a.lockprobe) === "probe-".concat(RUN), "fields.lockprobe=".concat(((_b = enduser === null || enduser === void 0 ? void 0 : enduser.fields) === null || _b === void 0 ? void 0 : _b.lockprobe) || '(unset)', " expected=probe-").concat(RUN));
|
|
736
|
+
record('G10', 'email still blocked while other fields update', (enduser === null || enduser === void 0 ? void 0 : enduser.email) === originalEmail, "email=".concat((enduser === null || enduser === void 0 ? void 0 : enduser.email) || '(unset)', " expected=").concat(originalEmail));
|
|
737
|
+
return [2 /*return*/];
|
|
738
|
+
}
|
|
739
|
+
});
|
|
740
|
+
}); })
|
|
741
|
+
// ---------------------------------------------------------------------------------------------
|
|
742
|
+
// G11 — the explicit override is per-REQUEST, not a flag reset. A response formed by a guess keeps
|
|
743
|
+
// authFieldIntakeDisabled set, so the OTHER consumer of that flag
|
|
744
|
+
// (save_enduser_updates_for_form_response, reached by later staff edits) stays protected.
|
|
745
|
+
// ---------------------------------------------------------------------------------------------
|
|
746
|
+
];
|
|
747
|
+
case 13:
|
|
748
|
+
// ---------------------------------------------------------------------------------------------
|
|
749
|
+
// G10 — the narrowing is email/phone ONLY. Everything else the webhook parses must still update
|
|
750
|
+
// on a guessed match, otherwise this fix has quietly broken Formsort intake.
|
|
751
|
+
// ---------------------------------------------------------------------------------------------
|
|
752
|
+
_h.sent();
|
|
753
|
+
// ---------------------------------------------------------------------------------------------
|
|
754
|
+
// G11 — the explicit override is per-REQUEST, not a flag reset. A response formed by a guess keeps
|
|
755
|
+
// authFieldIntakeDisabled set, so the OTHER consumer of that flag
|
|
756
|
+
// (save_enduser_updates_for_form_response, reached by later staff edits) stays protected.
|
|
757
|
+
// ---------------------------------------------------------------------------------------------
|
|
758
|
+
return [4 /*yield*/, runScenario('G11', 'explicit override does not clear the stored flag', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
759
|
+
var phone, victim, newEmail, data, enduser, response;
|
|
760
|
+
return __generator(this, function (_a) {
|
|
761
|
+
switch (_a.label) {
|
|
762
|
+
case 0:
|
|
763
|
+
phone = nextPhone();
|
|
764
|
+
return [4 /*yield*/, makeVictim('g11', { email: emailFor('g11'), phone: phone })];
|
|
765
|
+
case 1:
|
|
766
|
+
victim = _a.sent();
|
|
767
|
+
newEmail = emailFor('g11-new');
|
|
768
|
+
// draft formed by a guess -> flagged
|
|
769
|
+
return [4 /*yield*/, postWebhook({ label: 'g11', finalized: false, answers: [{ key: 'phone', value: phone }] })];
|
|
770
|
+
case 2:
|
|
771
|
+
// draft formed by a guess -> flagged
|
|
772
|
+
_a.sent();
|
|
773
|
+
return [4 /*yield*/, postWebhook({
|
|
774
|
+
label: 'g11',
|
|
775
|
+
finalized: true,
|
|
776
|
+
enduserId: victim.id,
|
|
777
|
+
answers: [{ key: 'phone', value: phone }, { key: 'email', value: newEmail }],
|
|
778
|
+
})];
|
|
779
|
+
case 3:
|
|
780
|
+
data = (_a.sent()).data;
|
|
781
|
+
if (!matched('G11', 'explicit override does not clear the stored flag', data, victim.id))
|
|
782
|
+
return [2 /*return*/];
|
|
783
|
+
return [4 /*yield*/, pollEnduser(victim.id, function (e) { return e.email === newEmail; })];
|
|
784
|
+
case 4:
|
|
785
|
+
enduser = _a.sent();
|
|
786
|
+
record('G11', 'explicit override writes email even on a flagged response', (enduser === null || enduser === void 0 ? void 0 : enduser.email) === newEmail, "email=".concat((enduser === null || enduser === void 0 ? void 0 : enduser.email) || '(unset)', " expected=").concat(newEmail));
|
|
787
|
+
if (!data.formResponseId) {
|
|
788
|
+
skip('G11', 'stored flag survives the override', 'webhook returned no formResponseId');
|
|
789
|
+
return [2 /*return*/];
|
|
790
|
+
}
|
|
791
|
+
return [4 /*yield*/, sdk.api.form_responses.getOne(data.formResponseId).catch(function () { return null; })];
|
|
792
|
+
case 5:
|
|
793
|
+
response = _a.sent();
|
|
794
|
+
if (!response) {
|
|
795
|
+
skip('G11', 'stored flag survives the override', 'form response not readable');
|
|
796
|
+
}
|
|
797
|
+
else if (response.authFieldIntakeDisabled === undefined) {
|
|
798
|
+
// The field is deliberately absent from schema.ts; if reads project it away there is nothing
|
|
799
|
+
// to assert here, and the enforcement is already covered by the scenarios above.
|
|
800
|
+
skip('G11', 'stored flag survives the override', 'authFieldIntakeDisabled not exposed on read');
|
|
801
|
+
}
|
|
802
|
+
else {
|
|
803
|
+
record('G11', 'stored flag survives the override', response.authFieldIntakeDisabled === true, "authFieldIntakeDisabled=".concat(response.authFieldIntakeDisabled, " expected=true"));
|
|
804
|
+
}
|
|
805
|
+
return [2 /*return*/];
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
}); })
|
|
809
|
+
// ---------------------------------------------------------------------------------------------
|
|
810
|
+
// G12 — the Stripe grace. A contact the Stripe integration created minutes ago carries payment data
|
|
811
|
+
// only (email + name + stripeCustomerId, never a phone), so intake is the first place a phone number
|
|
812
|
+
// exists at all. enduser_is_provisional_stripe_record re-opens BLANK email/phone for one hour after
|
|
813
|
+
// that insert; see api/integrations/payments/stripe_provisional_enduser.ts.
|
|
814
|
+
//
|
|
815
|
+
// The Stripe webhook cannot be driven locally (it needs a signature and an integration record), but
|
|
816
|
+
// nothing about the grace depends on Stripe having done the insert: source and stripeCustomerId are
|
|
817
|
+
// both enduserUpdatesDisabled, so an admin can create the same shape and drive the real webhook
|
|
818
|
+
// against it. A fixture created here always has a fresh ObjectId, so this exercises the in-window
|
|
819
|
+
// branch; the expired branch is pinned by the unit tests in
|
|
820
|
+
// api/integrations/payments/stripe_provisional_enduser.test.ts.
|
|
821
|
+
// ---------------------------------------------------------------------------------------------
|
|
822
|
+
];
|
|
823
|
+
case 14:
|
|
824
|
+
// ---------------------------------------------------------------------------------------------
|
|
825
|
+
// G11 — the explicit override is per-REQUEST, not a flag reset. A response formed by a guess keeps
|
|
826
|
+
// authFieldIntakeDisabled set, so the OTHER consumer of that flag
|
|
827
|
+
// (save_enduser_updates_for_form_response, reached by later staff edits) stays protected.
|
|
828
|
+
// ---------------------------------------------------------------------------------------------
|
|
829
|
+
_h.sent();
|
|
830
|
+
stripeVictim = function (label, fields) { return makeVictim(label, __assign({ source: 'Stripe', stripeCustomerId: "cus_".concat(RUN).concat(label) }, fields)); };
|
|
831
|
+
return [4 /*yield*/, runScenario('G12', 'blank phone on a fresh Stripe contact is filled from intake', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
832
|
+
var email, victim, intakePhone, data, enduser;
|
|
833
|
+
return __generator(this, function (_a) {
|
|
834
|
+
switch (_a.label) {
|
|
835
|
+
case 0:
|
|
836
|
+
email = emailFor('g12');
|
|
837
|
+
return [4 /*yield*/, stripeVictim('g12', { email: email, fname: 'Stripe', lname: "Payer".concat(RUN) })];
|
|
838
|
+
case 1:
|
|
839
|
+
victim = _a.sent();
|
|
840
|
+
intakePhone = nextPhone();
|
|
841
|
+
return [4 /*yield*/, postWebhook({
|
|
842
|
+
label: 'g12',
|
|
843
|
+
finalized: true,
|
|
844
|
+
answers: [
|
|
845
|
+
{ key: 'email', value: email },
|
|
846
|
+
{ key: 'phone', value: intakePhone },
|
|
847
|
+
{ key: 'dateOfBirth', value: '1990-01-02' },
|
|
848
|
+
{ key: 'ts_enduser_stripeprobe', value: "probe-".concat(RUN) },
|
|
849
|
+
],
|
|
850
|
+
})];
|
|
851
|
+
case 2:
|
|
852
|
+
data = (_a.sent()).data;
|
|
853
|
+
if (!matched('G12', 'blank phone on a fresh Stripe contact is filled from intake', data, victim.id))
|
|
854
|
+
return [2 /*return*/];
|
|
855
|
+
return [4 /*yield*/, pollEnduser(victim.id, function (e) { return e.phone === intakePhone; })];
|
|
856
|
+
case 3:
|
|
857
|
+
enduser = _a.sent();
|
|
858
|
+
record('G12', 'blank phone on a fresh Stripe contact is filled from intake', (enduser === null || enduser === void 0 ? void 0 : enduser.phone) === intakePhone, "phone=".concat((enduser === null || enduser === void 0 ? void 0 : enduser.phone) || '(unset)', " expected=").concat(intakePhone));
|
|
859
|
+
// the grace must not disturb the ordinary update path around it
|
|
860
|
+
record('G12', 'non-auth fields still land alongside the graced phone', (enduser === null || enduser === void 0 ? void 0 : enduser.dateOfBirth) === '01-02-1990', "dateOfBirth=".concat((enduser === null || enduser === void 0 ? void 0 : enduser.dateOfBirth) || '(unset)', " expected=01-02-1990"));
|
|
861
|
+
record('G12', 'email is unchanged by the grace', (enduser === null || enduser === void 0 ? void 0 : enduser.email) === email, "email=".concat((enduser === null || enduser === void 0 ? void 0 : enduser.email) || '(unset)', " expected=").concat(email));
|
|
862
|
+
return [2 /*return*/];
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
}); })
|
|
866
|
+
// ---------------------------------------------------------------------------------------------
|
|
867
|
+
// R12 / R13 — the grace is strictly BLANK-FILL. Repointing a stored email or phone is the takeover
|
|
868
|
+
// primitive this lock exists to stop, and being minutes old does not make it safe.
|
|
869
|
+
// ---------------------------------------------------------------------------------------------
|
|
870
|
+
];
|
|
871
|
+
case 15:
|
|
872
|
+
_h.sent();
|
|
873
|
+
// ---------------------------------------------------------------------------------------------
|
|
874
|
+
// R12 / R13 — the grace is strictly BLANK-FILL. Repointing a stored email or phone is the takeover
|
|
875
|
+
// primitive this lock exists to stop, and being minutes old does not make it safe.
|
|
876
|
+
// ---------------------------------------------------------------------------------------------
|
|
877
|
+
return [4 /*yield*/, runScenario('R12', 'grace does not overwrite a phone already on a Stripe contact', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
878
|
+
var email, originalPhone, victim, attackerPhone, data, enduser;
|
|
879
|
+
return __generator(this, function (_a) {
|
|
880
|
+
switch (_a.label) {
|
|
881
|
+
case 0:
|
|
882
|
+
email = emailFor('r12');
|
|
883
|
+
originalPhone = nextPhone();
|
|
884
|
+
return [4 /*yield*/, stripeVictim('r12', { email: email, phone: originalPhone })];
|
|
885
|
+
case 1:
|
|
886
|
+
victim = _a.sent();
|
|
887
|
+
attackerPhone = nextPhone();
|
|
888
|
+
return [4 /*yield*/, postWebhook({
|
|
889
|
+
label: 'r12',
|
|
890
|
+
finalized: true,
|
|
891
|
+
answers: [{ key: 'email', value: email }, { key: 'phone', value: attackerPhone }],
|
|
892
|
+
})];
|
|
893
|
+
case 2:
|
|
894
|
+
data = (_a.sent()).data;
|
|
895
|
+
if (!matched('R12', 'grace does not overwrite a phone already on a Stripe contact', data, victim.id))
|
|
896
|
+
return [2 /*return*/];
|
|
897
|
+
return [4 /*yield*/, pollEnduser(victim.id, function (e) { return e.phone === attackerPhone; })];
|
|
898
|
+
case 3:
|
|
899
|
+
enduser = _a.sent();
|
|
900
|
+
record('R12', 'grace does not overwrite a phone already on a Stripe contact', (enduser === null || enduser === void 0 ? void 0 : enduser.phone) === originalPhone, "phone=".concat((enduser === null || enduser === void 0 ? void 0 : enduser.phone) || '(unset)', " expected=").concat(originalPhone));
|
|
901
|
+
return [2 /*return*/];
|
|
902
|
+
}
|
|
903
|
+
});
|
|
904
|
+
}); })];
|
|
905
|
+
case 16:
|
|
906
|
+
// ---------------------------------------------------------------------------------------------
|
|
907
|
+
// R12 / R13 — the grace is strictly BLANK-FILL. Repointing a stored email or phone is the takeover
|
|
908
|
+
// primitive this lock exists to stop, and being minutes old does not make it safe.
|
|
909
|
+
// ---------------------------------------------------------------------------------------------
|
|
910
|
+
_h.sent();
|
|
911
|
+
return [4 /*yield*/, runScenario('R13', 'grace does not repoint the email of a Stripe contact', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
912
|
+
var originalEmail, phone, victim, attackerEmail, data, enduser;
|
|
913
|
+
return __generator(this, function (_a) {
|
|
914
|
+
switch (_a.label) {
|
|
915
|
+
case 0:
|
|
916
|
+
originalEmail = emailFor('r13');
|
|
917
|
+
phone = nextPhone();
|
|
918
|
+
return [4 /*yield*/, stripeVictim('r13', { email: originalEmail, phone: phone })];
|
|
919
|
+
case 1:
|
|
920
|
+
victim = _a.sent();
|
|
921
|
+
attackerEmail = emailFor('r13-attacker');
|
|
922
|
+
return [4 /*yield*/, postWebhook({
|
|
923
|
+
label: 'r13',
|
|
924
|
+
finalized: true,
|
|
925
|
+
answers: [{ key: 'phone', value: phone }, { key: 'email', value: attackerEmail }],
|
|
926
|
+
})];
|
|
927
|
+
case 2:
|
|
928
|
+
data = (_a.sent()).data;
|
|
929
|
+
if (!matched('R13', 'grace does not repoint the email of a Stripe contact', data, victim.id))
|
|
930
|
+
return [2 /*return*/];
|
|
931
|
+
return [4 /*yield*/, pollEnduser(victim.id, function (e) { return e.email === attackerEmail; })];
|
|
932
|
+
case 3:
|
|
933
|
+
enduser = _a.sent();
|
|
934
|
+
record('R13', 'grace does not repoint the email of a Stripe contact', (enduser === null || enduser === void 0 ? void 0 : enduser.email) === originalEmail, "email=".concat((enduser === null || enduser === void 0 ? void 0 : enduser.email) || '(unset)', " expected=").concat(originalEmail));
|
|
935
|
+
return [2 /*return*/];
|
|
936
|
+
}
|
|
937
|
+
});
|
|
938
|
+
}); })
|
|
939
|
+
// ---------------------------------------------------------------------------------------------
|
|
940
|
+
// R14 — BOTH markers are load-bearing. `source` is set by every other integration and by staff, and
|
|
941
|
+
// `stripeCustomerId` can be attached to a contact this integration never created, so neither alone
|
|
942
|
+
// may open the grace.
|
|
943
|
+
// ---------------------------------------------------------------------------------------------
|
|
944
|
+
];
|
|
945
|
+
case 17:
|
|
946
|
+
_h.sent();
|
|
947
|
+
// ---------------------------------------------------------------------------------------------
|
|
948
|
+
// R14 — BOTH markers are load-bearing. `source` is set by every other integration and by staff, and
|
|
949
|
+
// `stripeCustomerId` can be attached to a contact this integration never created, so neither alone
|
|
950
|
+
// may open the grace.
|
|
951
|
+
// ---------------------------------------------------------------------------------------------
|
|
952
|
+
return [4 /*yield*/, runScenario('R14', 'stripeCustomerId without a Stripe source does not open the grace', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
953
|
+
var email, victim, attackerPhone, data, enduser;
|
|
954
|
+
return __generator(this, function (_a) {
|
|
955
|
+
switch (_a.label) {
|
|
956
|
+
case 0:
|
|
957
|
+
email = emailFor('r14a');
|
|
958
|
+
return [4 /*yield*/, makeVictim('r14a', { email: email, source: 'Elation', stripeCustomerId: "cus_".concat(RUN, "r14a") })];
|
|
959
|
+
case 1:
|
|
960
|
+
victim = _a.sent();
|
|
961
|
+
attackerPhone = nextPhone();
|
|
962
|
+
return [4 /*yield*/, postWebhook({
|
|
963
|
+
label: 'r14a',
|
|
964
|
+
finalized: true,
|
|
965
|
+
answers: [{ key: 'email', value: email }, { key: 'phone', value: attackerPhone }],
|
|
966
|
+
})];
|
|
967
|
+
case 2:
|
|
968
|
+
data = (_a.sent()).data;
|
|
969
|
+
if (!matched('R14', 'stripeCustomerId without a Stripe source does not open the grace', data, victim.id))
|
|
970
|
+
return [2 /*return*/];
|
|
971
|
+
return [4 /*yield*/, pollEnduser(victim.id, function (e) { return e.phone === attackerPhone; })];
|
|
972
|
+
case 3:
|
|
973
|
+
enduser = _a.sent();
|
|
974
|
+
record('R14', 'stripeCustomerId without a Stripe source does not open the grace', !(enduser === null || enduser === void 0 ? void 0 : enduser.phone), "phone=".concat((enduser === null || enduser === void 0 ? void 0 : enduser.phone) || '(unset)', " expected=(unset)"));
|
|
975
|
+
return [2 /*return*/];
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
}); })];
|
|
979
|
+
case 18:
|
|
980
|
+
// ---------------------------------------------------------------------------------------------
|
|
981
|
+
// R14 — BOTH markers are load-bearing. `source` is set by every other integration and by staff, and
|
|
982
|
+
// `stripeCustomerId` can be attached to a contact this integration never created, so neither alone
|
|
983
|
+
// may open the grace.
|
|
984
|
+
// ---------------------------------------------------------------------------------------------
|
|
985
|
+
_h.sent();
|
|
986
|
+
return [4 /*yield*/, runScenario('R14', 'Stripe source without a stripeCustomerId does not open the grace', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
987
|
+
var email, victim, attackerPhone, data, enduser;
|
|
988
|
+
return __generator(this, function (_a) {
|
|
989
|
+
switch (_a.label) {
|
|
990
|
+
case 0:
|
|
991
|
+
email = emailFor('r14b');
|
|
992
|
+
return [4 /*yield*/, makeVictim('r14b', { email: email, source: 'Stripe' })];
|
|
993
|
+
case 1:
|
|
994
|
+
victim = _a.sent();
|
|
995
|
+
attackerPhone = nextPhone();
|
|
996
|
+
return [4 /*yield*/, postWebhook({
|
|
997
|
+
label: 'r14b',
|
|
998
|
+
finalized: true,
|
|
999
|
+
answers: [{ key: 'email', value: email }, { key: 'phone', value: attackerPhone }],
|
|
1000
|
+
})];
|
|
1001
|
+
case 2:
|
|
1002
|
+
data = (_a.sent()).data;
|
|
1003
|
+
if (!matched('R14', 'Stripe source without a stripeCustomerId does not open the grace', data, victim.id))
|
|
1004
|
+
return [2 /*return*/];
|
|
1005
|
+
return [4 /*yield*/, pollEnduser(victim.id, function (e) { return e.phone === attackerPhone; })];
|
|
1006
|
+
case 3:
|
|
1007
|
+
enduser = _a.sent();
|
|
1008
|
+
record('R14', 'Stripe source without a stripeCustomerId does not open the grace', !(enduser === null || enduser === void 0 ? void 0 : enduser.phone), "phone=".concat((enduser === null || enduser === void 0 ? void 0 : enduser.phone) || '(unset)', " expected=(unset)"));
|
|
1009
|
+
return [2 /*return*/];
|
|
1010
|
+
}
|
|
1011
|
+
});
|
|
1012
|
+
}); })
|
|
1013
|
+
// ---------------------------------------------------------------------------------------------
|
|
1014
|
+
// R15 — the grace is a per-REQUEST override, exactly like the explicit ?enduserId= hatch in G11: the
|
|
1015
|
+
// response still records that it attached to a pre-existing contact, so the other consumer of that
|
|
1016
|
+
// flag (save_enduser_updates_for_form_response, reached by later staff edits and by submissions
|
|
1017
|
+
// after the window closes) stays protected.
|
|
1018
|
+
// ---------------------------------------------------------------------------------------------
|
|
1019
|
+
];
|
|
1020
|
+
case 19:
|
|
1021
|
+
_h.sent();
|
|
1022
|
+
// ---------------------------------------------------------------------------------------------
|
|
1023
|
+
// R15 — the grace is a per-REQUEST override, exactly like the explicit ?enduserId= hatch in G11: the
|
|
1024
|
+
// response still records that it attached to a pre-existing contact, so the other consumer of that
|
|
1025
|
+
// flag (save_enduser_updates_for_form_response, reached by later staff edits and by submissions
|
|
1026
|
+
// after the window closes) stays protected.
|
|
1027
|
+
// ---------------------------------------------------------------------------------------------
|
|
1028
|
+
return [4 /*yield*/, runScenario('R15', 'grace does not clear the stored flag', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1029
|
+
var email, victim, intakePhone, data, enduser, response;
|
|
1030
|
+
return __generator(this, function (_a) {
|
|
1031
|
+
switch (_a.label) {
|
|
1032
|
+
case 0:
|
|
1033
|
+
email = emailFor('r15');
|
|
1034
|
+
return [4 /*yield*/, stripeVictim('r15', { email: email })];
|
|
1035
|
+
case 1:
|
|
1036
|
+
victim = _a.sent();
|
|
1037
|
+
intakePhone = nextPhone();
|
|
1038
|
+
return [4 /*yield*/, postWebhook({
|
|
1039
|
+
label: 'r15',
|
|
1040
|
+
finalized: true,
|
|
1041
|
+
answers: [{ key: 'email', value: email }, { key: 'phone', value: intakePhone }],
|
|
1042
|
+
})];
|
|
1043
|
+
case 2:
|
|
1044
|
+
data = (_a.sent()).data;
|
|
1045
|
+
if (!matched('R15', 'grace does not clear the stored flag', data, victim.id))
|
|
1046
|
+
return [2 /*return*/];
|
|
1047
|
+
return [4 /*yield*/, pollEnduser(victim.id, function (e) { return e.phone === intakePhone; })];
|
|
1048
|
+
case 3:
|
|
1049
|
+
enduser = _a.sent();
|
|
1050
|
+
record('R15', 'grace landed the phone (probe precondition)', (enduser === null || enduser === void 0 ? void 0 : enduser.phone) === intakePhone, "phone=".concat((enduser === null || enduser === void 0 ? void 0 : enduser.phone) || '(unset)', " expected=").concat(intakePhone));
|
|
1051
|
+
if (!data.formResponseId) {
|
|
1052
|
+
skip('R15', 'grace does not clear the stored flag', 'webhook returned no formResponseId');
|
|
1053
|
+
return [2 /*return*/];
|
|
1054
|
+
}
|
|
1055
|
+
return [4 /*yield*/, sdk.api.form_responses.getOne(data.formResponseId).catch(function () { return null; })];
|
|
1056
|
+
case 4:
|
|
1057
|
+
response = _a.sent();
|
|
1058
|
+
if (!response) {
|
|
1059
|
+
skip('R15', 'grace does not clear the stored flag', 'form response not readable');
|
|
1060
|
+
}
|
|
1061
|
+
else if (response.authFieldIntakeDisabled === undefined) {
|
|
1062
|
+
// deliberately absent from schema.ts; if reads project it away there is nothing to assert here
|
|
1063
|
+
skip('R15', 'grace does not clear the stored flag', 'authFieldIntakeDisabled not exposed on read');
|
|
1064
|
+
}
|
|
1065
|
+
else {
|
|
1066
|
+
record('R15', 'grace does not clear the stored flag', response.authFieldIntakeDisabled === true, "authFieldIntakeDisabled=".concat(response.authFieldIntakeDisabled, " expected=true"));
|
|
1067
|
+
}
|
|
1068
|
+
return [2 /*return*/];
|
|
1069
|
+
}
|
|
1070
|
+
});
|
|
1071
|
+
}); })
|
|
1072
|
+
// ---------------------------------------------------------------------------------------------
|
|
1073
|
+
// Report
|
|
1074
|
+
// ---------------------------------------------------------------------------------------------
|
|
1075
|
+
];
|
|
1076
|
+
case 20:
|
|
1077
|
+
// ---------------------------------------------------------------------------------------------
|
|
1078
|
+
// R15 — the grace is a per-REQUEST override, exactly like the explicit ?enduserId= hatch in G11: the
|
|
1079
|
+
// response still records that it attached to a pre-existing contact, so the other consumer of that
|
|
1080
|
+
// flag (save_enduser_updates_for_form_response, reached by later staff edits and by submissions
|
|
1081
|
+
// after the window closes) stays protected.
|
|
1082
|
+
// ---------------------------------------------------------------------------------------------
|
|
1083
|
+
_h.sent();
|
|
1084
|
+
failures = results.filter(function (r) { return !r.ok; });
|
|
1085
|
+
skipped = results.filter(function (r) { return !!r.skipped; });
|
|
1086
|
+
console.log('');
|
|
1087
|
+
console.log(' ┌─ Formsort webhook auth-field lock — summary ───────────────────────────');
|
|
1088
|
+
for (_i = 0, results_1 = results; _i < results_1.length; _i++) {
|
|
1089
|
+
r = results_1[_i];
|
|
1090
|
+
status_1 = r.skipped ? 'SKIP' : r.ok ? 'PASS' : 'FAIL';
|
|
1091
|
+
console.log(" \u2502 ".concat(status_1, " ").concat(r.group.padEnd(3), " ").concat(r.name).concat(r.detail ? " \u2014 ".concat(r.detail) : ''));
|
|
1092
|
+
}
|
|
1093
|
+
console.log(" \u2514\u2500 ".concat(results.length - failures.length, "/").concat(results.length, " passed, ").concat(skipped.length, " skipped, ").concat(failures.length, " failed"));
|
|
1094
|
+
console.log('');
|
|
1095
|
+
_b = 0, createdFormIds_1 = createdFormIds;
|
|
1096
|
+
_h.label = 21;
|
|
1097
|
+
case 21:
|
|
1098
|
+
if (!(_b < createdFormIds_1.length)) return [3 /*break*/, 27];
|
|
1099
|
+
formId = createdFormIds_1[_b];
|
|
1100
|
+
return [4 /*yield*/, sdk.api.form_responses.getSome({ filter: { formId: formId } }).catch(function () { return []; })];
|
|
1101
|
+
case 22:
|
|
1102
|
+
responses = _h.sent();
|
|
1103
|
+
_c = 0, responses_1 = responses;
|
|
1104
|
+
_h.label = 23;
|
|
1105
|
+
case 23:
|
|
1106
|
+
if (!(_c < responses_1.length)) return [3 /*break*/, 26];
|
|
1107
|
+
response = responses_1[_c];
|
|
1108
|
+
return [4 /*yield*/, sdk.api.form_responses.deleteOne(response.id).catch(function () { return null; })];
|
|
1109
|
+
case 24:
|
|
1110
|
+
_h.sent();
|
|
1111
|
+
_h.label = 25;
|
|
1112
|
+
case 25:
|
|
1113
|
+
_c++;
|
|
1114
|
+
return [3 /*break*/, 23];
|
|
1115
|
+
case 26:
|
|
1116
|
+
_b++;
|
|
1117
|
+
return [3 /*break*/, 21];
|
|
1118
|
+
case 27:
|
|
1119
|
+
_d = 0, createdFormIds_2 = createdFormIds;
|
|
1120
|
+
_h.label = 28;
|
|
1121
|
+
case 28:
|
|
1122
|
+
if (!(_d < createdFormIds_2.length)) return [3 /*break*/, 31];
|
|
1123
|
+
id = createdFormIds_2[_d];
|
|
1124
|
+
return [4 /*yield*/, sdk.api.forms.deleteOne(id).catch(function () { return null; })];
|
|
1125
|
+
case 29:
|
|
1126
|
+
_h.sent();
|
|
1127
|
+
_h.label = 30;
|
|
1128
|
+
case 30:
|
|
1129
|
+
_d++;
|
|
1130
|
+
return [3 /*break*/, 28];
|
|
1131
|
+
case 31:
|
|
1132
|
+
uniqueEnduserIds = createdEnduserIds.filter(function (id, i) { return !!id && createdEnduserIds.indexOf(id) === i; });
|
|
1133
|
+
_e = 0, uniqueEnduserIds_1 = uniqueEnduserIds;
|
|
1134
|
+
_h.label = 32;
|
|
1135
|
+
case 32:
|
|
1136
|
+
if (!(_e < uniqueEnduserIds_1.length)) return [3 /*break*/, 35];
|
|
1137
|
+
id = uniqueEnduserIds_1[_e];
|
|
1138
|
+
return [4 /*yield*/, sdk.api.endusers.deleteOne(id).catch(function () { return null; })];
|
|
1139
|
+
case 33:
|
|
1140
|
+
_h.sent();
|
|
1141
|
+
_h.label = 34;
|
|
1142
|
+
case 34:
|
|
1143
|
+
_e++;
|
|
1144
|
+
return [3 /*break*/, 32];
|
|
1145
|
+
case 35:
|
|
1146
|
+
_f = 0, createdApiKeyIds_1 = createdApiKeyIds;
|
|
1147
|
+
_h.label = 36;
|
|
1148
|
+
case 36:
|
|
1149
|
+
if (!(_f < createdApiKeyIds_1.length)) return [3 /*break*/, 39];
|
|
1150
|
+
id = createdApiKeyIds_1[_f];
|
|
1151
|
+
return [4 /*yield*/, sdk.api.api_keys.deleteOne(id).catch(function () { return null; })];
|
|
1152
|
+
case 37:
|
|
1153
|
+
_h.sent();
|
|
1154
|
+
_h.label = 38;
|
|
1155
|
+
case 38:
|
|
1156
|
+
_f++;
|
|
1157
|
+
return [3 /*break*/, 36];
|
|
1158
|
+
case 39:
|
|
1159
|
+
// ---------------------------------------------------------------------------------------------
|
|
1160
|
+
// Assert — single pass, last, so the full table above is always printed first.
|
|
1161
|
+
// ---------------------------------------------------------------------------------------------
|
|
1162
|
+
for (_g = 0, results_2 = results; _g < results_2.length; _g++) {
|
|
1163
|
+
r = results_2[_g];
|
|
1164
|
+
(0, testing_1.assert)(r.ok, "[".concat(r.group, "] ").concat(r.name, " \u2014 ").concat(r.detail), "[".concat(r.group, "] ").concat(r.name));
|
|
1165
|
+
}
|
|
1166
|
+
return [2 /*return*/];
|
|
1167
|
+
}
|
|
1168
|
+
});
|
|
1169
|
+
});
|
|
1170
|
+
};
|
|
1171
|
+
exports.formsort_webhook_auth_field_lock_tests = formsort_webhook_auth_field_lock_tests;
|
|
1172
|
+
// Allow running this test file independently
|
|
1173
|
+
if (require.main === module) {
|
|
1174
|
+
console.log("\uD83C\uDF10 Using API URL: ".concat(host));
|
|
1175
|
+
var sdk_2 = new sdk_1.Session({ host: host });
|
|
1176
|
+
var sdkNonAdmin_1 = new sdk_1.Session({ host: host });
|
|
1177
|
+
var runTests = function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
1178
|
+
return __generator(this, function (_a) {
|
|
1179
|
+
switch (_a.label) {
|
|
1180
|
+
case 0: return [4 /*yield*/, (0, setup_1.setup_tests)(sdk_2, sdkNonAdmin_1)];
|
|
1181
|
+
case 1:
|
|
1182
|
+
_a.sent();
|
|
1183
|
+
return [4 /*yield*/, (0, exports.formsort_webhook_auth_field_lock_tests)({ sdk: sdk_2, sdkNonAdmin: sdkNonAdmin_1 })];
|
|
1184
|
+
case 2:
|
|
1185
|
+
_a.sent();
|
|
1186
|
+
return [2 /*return*/];
|
|
1187
|
+
}
|
|
1188
|
+
});
|
|
1189
|
+
}); };
|
|
1190
|
+
runTests()
|
|
1191
|
+
.then(function () {
|
|
1192
|
+
console.log("✅ Formsort webhook auth field lock tests completed successfully");
|
|
1193
|
+
process.exit(0);
|
|
1194
|
+
})
|
|
1195
|
+
.catch(function (error) {
|
|
1196
|
+
console.error("❌ Formsort webhook auth field lock tests failed:", error);
|
|
1197
|
+
process.exit(1);
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
//# sourceMappingURL=formsort_webhook_auth_field_lock.test.js.map
|