@tellescope/sdk 1.256.14 → 1.256.16
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 +2 -2
- package/lib/cjs/tests/api_tests/auto_merge_form_submission.test.js +1 -1
- package/lib/cjs/tests/api_tests/auto_merge_form_submission.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/enduser_observations_auto_review.test.d.ts +5 -0
- package/lib/cjs/tests/api_tests/enduser_observations_auto_review.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/enduser_observations_auto_review.test.js +388 -0
- package/lib/cjs/tests/api_tests/enduser_observations_auto_review.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/integrations_redacted.test.js +3 -3
- package/lib/cjs/tests/api_tests/integrations_redacted.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/organization_signup.test.d.ts +1 -1
- package/lib/cjs/tests/api_tests/organization_signup.test.js +1 -1
- package/lib/cjs/tests/api_tests/purchase_made_trigger.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/purchase_made_trigger.test.js +103 -162
- package/lib/cjs/tests/api_tests/purchase_made_trigger.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/F-0200-form-logic-js-execution.test.d.ts +33 -0
- package/lib/cjs/tests/api_tests/security/F-0200-form-logic-js-execution.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0200-form-logic-js-execution.test.js +249 -0
- package/lib/cjs/tests/api_tests/security/F-0200-form-logic-js-execution.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/add-to-journey-foreign-enduserid.test.d.ts +42 -0
- package/lib/cjs/tests/api_tests/security/add-to-journey-foreign-enduserid.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/add-to-journey-foreign-enduserid.test.js +281 -0
- package/lib/cjs/tests/api_tests/security/add-to-journey-foreign-enduserid.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/ai-conversations-rbac.test.d.ts +38 -0
- package/lib/cjs/tests/api_tests/security/ai-conversations-rbac.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/ai-conversations-rbac.test.js +390 -0
- package/lib/cjs/tests/api_tests/security/ai-conversations-rbac.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/allowed-paths-confinement.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/security/allowed-paths-confinement.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/allowed-paths-confinement.test.js +1003 -0
- package/lib/cjs/tests/api_tests/security/allowed-paths-confinement.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/automated-actions-process-gate.test.d.ts +42 -0
- package/lib/cjs/tests/api_tests/security/automated-actions-process-gate.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/automated-actions-process-gate.test.js +323 -0
- package/lib/cjs/tests/api_tests/security/automated-actions-process-gate.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/booking-info-idor-token-mint.test.d.ts +29 -0
- package/lib/cjs/tests/api_tests/security/booking-info-idor-token-mint.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/booking-info-idor-token-mint.test.js +335 -0
- package/lib/cjs/tests/api_tests/security/booking-info-idor-token-mint.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/cascade-role-rename-cross-tenant.test.d.ts +32 -0
- package/lib/cjs/tests/api_tests/security/cascade-role-rename-cross-tenant.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/cascade-role-rename-cross-tenant.test.js +237 -0
- package/lib/cjs/tests/api_tests/security/cascade-role-rename-cross-tenant.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/data-sync-redaction-bypass.test.d.ts +28 -0
- package/lib/cjs/tests/api_tests/security/data-sync-redaction-bypass.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/data-sync-redaction-bypass.test.js +349 -0
- package/lib/cjs/tests/api_tests/security/data-sync-redaction-bypass.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/develop-health-benefit-verification-rbac.test.d.ts +30 -0
- package/lib/cjs/tests/api_tests/security/develop-health-benefit-verification-rbac.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/develop-health-benefit-verification-rbac.test.js +314 -0
- package/lib/cjs/tests/api_tests/security/develop-health-benefit-verification-rbac.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/enduser-cross-create.test.d.ts +21 -0
- package/lib/cjs/tests/api_tests/security/enduser-cross-create.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/enduser-cross-create.test.js +362 -0
- package/lib/cjs/tests/api_tests/security/enduser-cross-create.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/enduser-write-restrictions.test.d.ts +21 -0
- package/lib/cjs/tests/api_tests/security/enduser-write-restrictions.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/enduser-write-restrictions.test.js +366 -0
- package/lib/cjs/tests/api_tests/security/enduser-write-restrictions.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/file-link-session-confinement.test.d.ts +5 -0
- package/lib/cjs/tests/api_tests/security/file-link-session-confinement.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/file-link-session-confinement.test.js +357 -0
- package/lib/cjs/tests/api_tests/security/file-link-session-confinement.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/form-logic-js-execution.test.d.ts +32 -0
- package/lib/cjs/tests/api_tests/security/form-logic-js-execution.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/form-logic-js-execution.test.js +248 -0
- package/lib/cjs/tests/api_tests/security/form-logic-js-execution.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/globalunique-conflict-redaction.test.d.ts +19 -0
- package/lib/cjs/tests/api_tests/security/globalunique-conflict-redaction.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/globalunique-conflict-redaction.test.js +138 -0
- package/lib/cjs/tests/api_tests/security/globalunique-conflict-redaction.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/handle-incoming-communication-cross-tenant.test.d.ts +24 -0
- package/lib/cjs/tests/api_tests/security/handle-incoming-communication-cross-tenant.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/handle-incoming-communication-cross-tenant.test.js +201 -0
- package/lib/cjs/tests/api_tests/security/handle-incoming-communication-cross-tenant.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/invite-user-enumeration.test.d.ts +29 -0
- package/lib/cjs/tests/api_tests/security/invite-user-enumeration.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/invite-user-enumeration.test.js +278 -0
- package/lib/cjs/tests/api_tests/security/invite-user-enumeration.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/load-inbox-redaction.test.d.ts +17 -0
- package/lib/cjs/tests/api_tests/security/load-inbox-redaction.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/load-inbox-redaction.test.js +155 -0
- package/lib/cjs/tests/api_tests/security/load-inbox-redaction.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/prototype-pollution.test.d.ts +2 -0
- package/lib/cjs/tests/api_tests/security/prototype-pollution.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/prototype-pollution.test.js +88 -0
- package/lib/cjs/tests/api_tests/security/prototype-pollution.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/proxy-image-unauth-ssrf.test.d.ts +34 -0
- package/lib/cjs/tests/api_tests/security/proxy-image-unauth-ssrf.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/proxy-image-unauth-ssrf.test.js +230 -0
- package/lib/cjs/tests/api_tests/security/proxy-image-unauth-ssrf.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/sanitize-user-html-with-iframes.test.d.ts +2 -0
- package/lib/cjs/tests/api_tests/security/sanitize-user-html-with-iframes.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/sanitize-user-html-with-iframes.test.js +242 -0
- package/lib/cjs/tests/api_tests/security/sanitize-user-html-with-iframes.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/sanitize-user-html.test.d.ts +2 -0
- package/lib/cjs/tests/api_tests/security/sanitize-user-html.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/sanitize-user-html.test.js +148 -0
- package/lib/cjs/tests/api_tests/security/sanitize-user-html.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/schema-adminonly-enforcement.test.d.ts +45 -0
- package/lib/cjs/tests/api_tests/security/schema-adminonly-enforcement.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/schema-adminonly-enforcement.test.js +388 -0
- package/lib/cjs/tests/api_tests/security/schema-adminonly-enforcement.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/self-admin-role-assignment.test.d.ts +38 -0
- package/lib/cjs/tests/api_tests/security/self-admin-role-assignment.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/self-admin-role-assignment.test.js +222 -0
- package/lib/cjs/tests/api_tests/security/self-admin-role-assignment.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/startlinktoken-enduser-staff-escalation.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/security/startlinktoken-enduser-staff-escalation.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/startlinktoken-enduser-staff-escalation.test.js +354 -0
- package/lib/cjs/tests/api_tests/security/startlinktoken-enduser-staff-escalation.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/webhook-outbound-ssrf.test.d.ts +28 -0
- package/lib/cjs/tests/api_tests/security/webhook-outbound-ssrf.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/webhook-outbound-ssrf.test.js +442 -0
- package/lib/cjs/tests/api_tests/security/webhook-outbound-ssrf.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/webhook-timeout.test.d.ts +19 -0
- package/lib/cjs/tests/api_tests/security/webhook-timeout.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/webhook-timeout.test.js +156 -0
- package/lib/cjs/tests/api_tests/security/webhook-timeout.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/webhook_error_handling.test.js +2 -2
- package/lib/cjs/tests/api_tests/webhook_error_handling.test.js.map +1 -1
- package/lib/cjs/tests/tests.d.ts.map +1 -1
- package/lib/cjs/tests/tests.js +60 -46
- package/lib/cjs/tests/tests.js.map +1 -1
- package/lib/cjs/tests/unit_tests/openapi_spec_freshness.test.d.ts +1 -1
- package/lib/cjs/tests/unit_tests/openapi_spec_freshness.test.js +1 -1
- package/lib/cjs/tsconfig.cjs.tsbuildinfo +1 -0
- package/lib/esm/enduser.d.ts +0 -1
- package/lib/esm/enduser.d.ts.map +1 -1
- package/lib/esm/sdk.d.ts +4 -5
- package/lib/esm/sdk.d.ts.map +1 -1
- package/lib/esm/session.d.ts +0 -1
- package/lib/esm/session.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/auto_merge_form_submission.test.js +1 -1
- package/lib/esm/tests/api_tests/auto_merge_form_submission.test.js.map +1 -1
- package/lib/esm/tests/api_tests/enduser_observations_auto_review.test.d.ts +5 -0
- package/lib/esm/tests/api_tests/enduser_observations_auto_review.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/enduser_observations_auto_review.test.js +383 -0
- package/lib/esm/tests/api_tests/enduser_observations_auto_review.test.js.map +1 -0
- package/lib/esm/tests/api_tests/integrations_redacted.test.js +3 -3
- package/lib/esm/tests/api_tests/integrations_redacted.test.js.map +1 -1
- package/lib/esm/tests/api_tests/organization_signup.test.d.ts +1 -1
- package/lib/esm/tests/api_tests/organization_signup.test.js +1 -1
- package/lib/esm/tests/api_tests/purchase_made_trigger.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/purchase_made_trigger.test.js +104 -163
- package/lib/esm/tests/api_tests/purchase_made_trigger.test.js.map +1 -1
- package/lib/esm/tests/api_tests/security/F-0200-form-logic-js-execution.test.d.ts +33 -0
- package/lib/esm/tests/api_tests/security/F-0200-form-logic-js-execution.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0200-form-logic-js-execution.test.js +244 -0
- package/lib/esm/tests/api_tests/security/F-0200-form-logic-js-execution.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/add-to-journey-foreign-enduserid.test.d.ts +42 -0
- package/lib/esm/tests/api_tests/security/add-to-journey-foreign-enduserid.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/add-to-journey-foreign-enduserid.test.js +276 -0
- package/lib/esm/tests/api_tests/security/add-to-journey-foreign-enduserid.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/ai-conversations-rbac.test.d.ts +38 -0
- package/lib/esm/tests/api_tests/security/ai-conversations-rbac.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/ai-conversations-rbac.test.js +386 -0
- package/lib/esm/tests/api_tests/security/ai-conversations-rbac.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/allowed-paths-confinement.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/security/allowed-paths-confinement.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/allowed-paths-confinement.test.js +996 -0
- package/lib/esm/tests/api_tests/security/allowed-paths-confinement.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/automated-actions-process-gate.test.d.ts +42 -0
- package/lib/esm/tests/api_tests/security/automated-actions-process-gate.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/automated-actions-process-gate.test.js +316 -0
- package/lib/esm/tests/api_tests/security/automated-actions-process-gate.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/booking-info-idor-token-mint.test.d.ts +29 -0
- package/lib/esm/tests/api_tests/security/booking-info-idor-token-mint.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/booking-info-idor-token-mint.test.js +331 -0
- package/lib/esm/tests/api_tests/security/booking-info-idor-token-mint.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/cascade-role-rename-cross-tenant.test.d.ts +32 -0
- package/lib/esm/tests/api_tests/security/cascade-role-rename-cross-tenant.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/cascade-role-rename-cross-tenant.test.js +233 -0
- package/lib/esm/tests/api_tests/security/cascade-role-rename-cross-tenant.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/data-sync-redaction-bypass.test.d.ts +28 -0
- package/lib/esm/tests/api_tests/security/data-sync-redaction-bypass.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/data-sync-redaction-bypass.test.js +345 -0
- package/lib/esm/tests/api_tests/security/data-sync-redaction-bypass.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/develop-health-benefit-verification-rbac.test.d.ts +30 -0
- package/lib/esm/tests/api_tests/security/develop-health-benefit-verification-rbac.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/develop-health-benefit-verification-rbac.test.js +310 -0
- package/lib/esm/tests/api_tests/security/develop-health-benefit-verification-rbac.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/enduser-cross-create.test.d.ts +21 -0
- package/lib/esm/tests/api_tests/security/enduser-cross-create.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/enduser-cross-create.test.js +358 -0
- package/lib/esm/tests/api_tests/security/enduser-cross-create.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/enduser-write-restrictions.test.d.ts +21 -0
- package/lib/esm/tests/api_tests/security/enduser-write-restrictions.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/enduser-write-restrictions.test.js +362 -0
- package/lib/esm/tests/api_tests/security/enduser-write-restrictions.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/file-link-session-confinement.test.d.ts +5 -0
- package/lib/esm/tests/api_tests/security/file-link-session-confinement.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/file-link-session-confinement.test.js +349 -0
- package/lib/esm/tests/api_tests/security/file-link-session-confinement.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/form-logic-js-execution.test.d.ts +32 -0
- package/lib/esm/tests/api_tests/security/form-logic-js-execution.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/form-logic-js-execution.test.js +243 -0
- package/lib/esm/tests/api_tests/security/form-logic-js-execution.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/globalunique-conflict-redaction.test.d.ts +19 -0
- package/lib/esm/tests/api_tests/security/globalunique-conflict-redaction.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/globalunique-conflict-redaction.test.js +134 -0
- package/lib/esm/tests/api_tests/security/globalunique-conflict-redaction.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/handle-incoming-communication-cross-tenant.test.d.ts +24 -0
- package/lib/esm/tests/api_tests/security/handle-incoming-communication-cross-tenant.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/handle-incoming-communication-cross-tenant.test.js +194 -0
- package/lib/esm/tests/api_tests/security/handle-incoming-communication-cross-tenant.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/invite-user-enumeration.test.d.ts +29 -0
- package/lib/esm/tests/api_tests/security/invite-user-enumeration.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/invite-user-enumeration.test.js +271 -0
- package/lib/esm/tests/api_tests/security/invite-user-enumeration.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/load-inbox-redaction.test.d.ts +17 -0
- package/lib/esm/tests/api_tests/security/load-inbox-redaction.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/load-inbox-redaction.test.js +151 -0
- package/lib/esm/tests/api_tests/security/load-inbox-redaction.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/prototype-pollution.test.d.ts +2 -0
- package/lib/esm/tests/api_tests/security/prototype-pollution.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/prototype-pollution.test.js +84 -0
- package/lib/esm/tests/api_tests/security/prototype-pollution.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/proxy-image-unauth-ssrf.test.d.ts +34 -0
- package/lib/esm/tests/api_tests/security/proxy-image-unauth-ssrf.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/proxy-image-unauth-ssrf.test.js +223 -0
- package/lib/esm/tests/api_tests/security/proxy-image-unauth-ssrf.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/sanitize-user-html-with-iframes.test.d.ts +2 -0
- package/lib/esm/tests/api_tests/security/sanitize-user-html-with-iframes.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/sanitize-user-html-with-iframes.test.js +238 -0
- package/lib/esm/tests/api_tests/security/sanitize-user-html-with-iframes.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/sanitize-user-html.test.d.ts +2 -0
- package/lib/esm/tests/api_tests/security/sanitize-user-html.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/sanitize-user-html.test.js +144 -0
- package/lib/esm/tests/api_tests/security/sanitize-user-html.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/schema-adminonly-enforcement.test.d.ts +45 -0
- package/lib/esm/tests/api_tests/security/schema-adminonly-enforcement.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/schema-adminonly-enforcement.test.js +381 -0
- package/lib/esm/tests/api_tests/security/schema-adminonly-enforcement.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/self-admin-role-assignment.test.d.ts +38 -0
- package/lib/esm/tests/api_tests/security/self-admin-role-assignment.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/self-admin-role-assignment.test.js +218 -0
- package/lib/esm/tests/api_tests/security/self-admin-role-assignment.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/startlinktoken-enduser-staff-escalation.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/security/startlinktoken-enduser-staff-escalation.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/startlinktoken-enduser-staff-escalation.test.js +350 -0
- package/lib/esm/tests/api_tests/security/startlinktoken-enduser-staff-escalation.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/webhook-outbound-ssrf.test.d.ts +28 -0
- package/lib/esm/tests/api_tests/security/webhook-outbound-ssrf.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/webhook-outbound-ssrf.test.js +435 -0
- package/lib/esm/tests/api_tests/security/webhook-outbound-ssrf.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/webhook-timeout.test.d.ts +19 -0
- package/lib/esm/tests/api_tests/security/webhook-timeout.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/webhook-timeout.test.js +149 -0
- package/lib/esm/tests/api_tests/security/webhook-timeout.test.js.map +1 -0
- package/lib/esm/tests/api_tests/webhook_error_handling.test.js +2 -2
- package/lib/esm/tests/api_tests/webhook_error_handling.test.js.map +1 -1
- package/lib/esm/tests/tests.d.ts.map +1 -1
- package/lib/esm/tests/tests.js +38 -24
- package/lib/esm/tests/tests.js.map +1 -1
- package/lib/esm/tests/unit_tests/openapi_spec_freshness.test.d.ts +1 -1
- package/lib/esm/tests/unit_tests/openapi_spec_freshness.test.js +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +10 -10
- package/src/tests/api_tests/auto_merge_form_submission.test.ts +1 -1
- package/src/tests/api_tests/enduser_observations_auto_review.test.ts +358 -0
- package/src/tests/api_tests/integrations_redacted.test.ts +1 -1
- package/src/tests/api_tests/organization_signup.test.ts +2 -2
- package/src/tests/api_tests/purchase_made_trigger.test.ts +29 -41
- package/src/tests/api_tests/security/{F-0196-add-to-journey-foreign-enduserid.test.ts → add-to-journey-foreign-enduserid.test.ts} +6 -7
- package/src/tests/api_tests/security/{F-0005-ai-conversations-rbac.test.ts → ai-conversations-rbac.test.ts} +14 -14
- package/src/tests/api_tests/security/{F-0166-allowed-paths-wildcard-bypass.test.ts → allowed-paths-confinement.test.ts} +11 -11
- package/src/tests/api_tests/security/{F-0192-automated-actions-process-gate.test.ts → automated-actions-process-gate.test.ts} +6 -7
- package/src/tests/api_tests/security/{F-0179-booking-info-idor-token-mint.test.ts → booking-info-idor-token-mint.test.ts} +5 -6
- package/src/tests/api_tests/security/{F-0053-cascade-role-rename-cross-tenant.test.ts → cascade-role-rename-cross-tenant.test.ts} +10 -10
- package/src/tests/api_tests/security/{F-0001-data-sync-redaction-bypass.test.ts → data-sync-redaction-bypass.test.ts} +9 -9
- package/src/tests/api_tests/security/{F-0006-develop-health-benefit-verification-rbac.test.ts → develop-health-benefit-verification-rbac.test.ts} +7 -8
- package/src/tests/api_tests/security/{F-0116-F-0119-enduser-cross-create.test.ts → enduser-cross-create.test.ts} +25 -25
- package/src/tests/api_tests/security/{F-0106-F-0110-enduser-write-restrictions.test.ts → enduser-write-restrictions.test.ts} +12 -14
- package/src/tests/api_tests/security/file-link-session-confinement.test.ts +291 -0
- package/src/tests/api_tests/security/form-logic-js-execution.test.ts +183 -0
- package/src/tests/api_tests/security/{F-0145-globalunique-leak.test.ts → globalunique-conflict-redaction.test.ts} +5 -6
- package/src/tests/api_tests/security/{F-0008-handle-incoming-communication-cross-tenant.test.ts → handle-incoming-communication-cross-tenant.test.ts} +8 -8
- package/src/tests/api_tests/security/{F-0007-invite-user-enumeration.test.ts → invite-user-enumeration.test.ts} +8 -8
- package/src/tests/api_tests/security/{F-0151-load-inbox-redaction.test.ts → load-inbox-redaction.test.ts} +5 -6
- package/src/tests/api_tests/security/{F-0016-prototype-pollution.test.ts → prototype-pollution.test.ts} +4 -4
- package/src/tests/api_tests/security/{F-0025-proxy-image-unauth-ssrf.test.ts → proxy-image-unauth-ssrf.test.ts} +11 -12
- package/src/tests/api_tests/security/{F-0013-sanitize-user-html-with-iframes.test.ts → sanitize-user-html-with-iframes.test.ts} +5 -5
- package/src/tests/api_tests/security/{F-0013-sanitize-user-html.test.ts → sanitize-user-html.test.ts} +4 -4
- package/src/tests/api_tests/security/{F-0191-schema-adminonly-enforcement.test.ts → schema-adminonly-enforcement.test.ts} +6 -7
- package/src/tests/api_tests/security/{F-0076-self-admin-role-assignment.test.ts → self-admin-role-assignment.test.ts} +13 -13
- package/src/tests/api_tests/security/{F-0185-startlinktoken-enduser-staff-escalation.test.ts → startlinktoken-enduser-staff-escalation.test.ts} +6 -6
- package/src/tests/api_tests/security/{F-0015-webhook-outbound-ssrf.test.ts → webhook-outbound-ssrf.test.ts} +13 -14
- package/src/tests/api_tests/security/{F-0155-webhook-timeout.test.ts → webhook-timeout.test.ts} +6 -7
- package/src/tests/api_tests/webhook_error_handling.test.ts +1 -1
- package/src/tests/tests.ts +43 -24
- package/src/tests/unit_tests/openapi_spec_freshness.test.ts +1 -1
- package/test_generated.pdf +0 -0
- package/src/tests/api_tests/security/readonly-enduser-preview.test.ts +0 -301
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
require('source-map-support').install();
|
|
2
|
+
|
|
3
|
+
import axios from "axios"
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
assert_checks,
|
|
7
|
+
Check,
|
|
8
|
+
setup_isolated_tenant,
|
|
9
|
+
} from "../../setup_isolated"
|
|
10
|
+
|
|
11
|
+
const host = process.env.API_URL || 'http://localhost:8080' as const
|
|
12
|
+
|
|
13
|
+
const RAND = () => Math.random().toString(36).slice(2, 10)
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The `{{files.<id>.link}}` merge field must mint a session that can download ONE file, and nothing
|
|
17
|
+
* else.
|
|
18
|
+
*
|
|
19
|
+
* That merge field is how the webapp's "Generate PDF -> Save and Share -> Email" flow on the enduser
|
|
20
|
+
* timeline (DownloadFormResponse.tsx, ComposeSharePDFEmail) delivers a document to a hand-typed
|
|
21
|
+
* address — a referral contact, a caregiver, an outside provider. The link is resolved at send time
|
|
22
|
+
* by message_templating.ts, which called login_enduser with no confinement at all, so the recipient
|
|
23
|
+
* received a full 24-hour patient-portal session: chat history, every form response, every file,
|
|
24
|
+
* observations, medications, and every write route including PATCH /enduser/:id.
|
|
25
|
+
*
|
|
26
|
+
* Route access was not the only leak. Both portals call refresh-enduser-session during URL-token
|
|
27
|
+
* intake, and for a session not flagged `fromPublicSession` that handler returns the whole patient
|
|
28
|
+
* document (name, dateOfBirth, email, phone, insurance, fields, notes) — so the chart was handed
|
|
29
|
+
* over on page load, before a single route was touched. Probe 7b covers that.
|
|
30
|
+
*
|
|
31
|
+
* Two variants, because one bit — is the recipient the patient? — drives two behaviours the merge
|
|
32
|
+
* field previously could not express:
|
|
33
|
+
*
|
|
34
|
+
* `link` goes to the patient. Keeps the org's MFA policy, keeps OTP recovery.
|
|
35
|
+
* `shared-link` goes to someone who is NOT the patient. An OTP code is delivered to the PATIENT's
|
|
36
|
+
* own email or phone, so for this variant recovery is both unanswerable by the
|
|
37
|
+
* holder and a way to make the platform message the patient on demand. It is
|
|
38
|
+
* refused, and the token is minted bypassMFA so it never needs a challenge either.
|
|
39
|
+
*
|
|
40
|
+
* Probe 12 is the one that decides whether the OTP rule is real: the three OTP endpoints read their
|
|
41
|
+
* `token` argument with `jwtDecode`, which does not verify the signature. The file link puts a
|
|
42
|
+
* decodable JWT in the recipient's URL bar, so they know the enduser id, and a scope name in an
|
|
43
|
+
* unverified token is a suggestion rather than a claim. Without signature verification the rule is
|
|
44
|
+
* a paper wall.
|
|
45
|
+
*
|
|
46
|
+
* Every confinement probe is paired against the tenant's own admin session, so a red check means
|
|
47
|
+
* confinement rather than a broken fixture.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
type Probe = { status: number, body: any }
|
|
51
|
+
|
|
52
|
+
const probe = async (
|
|
53
|
+
method: 'get' | 'post' | 'patch',
|
|
54
|
+
path: string,
|
|
55
|
+
o: { token?: string, params?: object, data?: object } = {},
|
|
56
|
+
): Promise<Probe> => {
|
|
57
|
+
const r = await axios.request({
|
|
58
|
+
method,
|
|
59
|
+
url: `${host}/v1${path}`,
|
|
60
|
+
params: o.params,
|
|
61
|
+
data: o.data,
|
|
62
|
+
headers: o.token ? { Authorization: `Bearer ${o.token}` } : undefined,
|
|
63
|
+
validateStatus: () => true,
|
|
64
|
+
})
|
|
65
|
+
return { status: r.status, body: r.data }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const file_link_session_confinement_checks = async (): Promise<Check[]> => {
|
|
69
|
+
const checks: Check[] = []
|
|
70
|
+
const record = (label: string, pass: boolean, detail: string) => checks.push({ label, pass, detail })
|
|
71
|
+
|
|
72
|
+
let tenant: Awaited<ReturnType<typeof setup_isolated_tenant>> | undefined
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
tenant = await setup_isolated_tenant({
|
|
76
|
+
name: `FileLink ${RAND()}`, users: [{ isAdmin: true }], apiKeys: 0,
|
|
77
|
+
})
|
|
78
|
+
const sdk = tenant.sdk
|
|
79
|
+
|
|
80
|
+
// dontsend- so the OTP probes cannot deliver anything real, same convention the provisioner uses
|
|
81
|
+
const enduser = await sdk.api.endusers.createOne({
|
|
82
|
+
fname: 'FileLink', lname: 'Recipient',
|
|
83
|
+
email: `dontsend-filelink-${RAND()}@tellescope.com`,
|
|
84
|
+
phone: '+15555550100',
|
|
85
|
+
dateOfBirth: '01-01-1990',
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
// No S3 upload needed: prepare_file_upload inserts the files record with its secureName
|
|
89
|
+
// immediately, and file_download_URL only SIGNS a GET url — it never checks the object exists.
|
|
90
|
+
const newFile = async (name: string) => (
|
|
91
|
+
(await sdk.api.files.prepare_file_upload({
|
|
92
|
+
name, type: 'application/pdf', size: 1000, enduserId: enduser.id,
|
|
93
|
+
})).file
|
|
94
|
+
)
|
|
95
|
+
const fileA = await newFile(`shared-${RAND()}.pdf`) // the one that gets shared
|
|
96
|
+
const fileB = await newFile(`private-${RAND()}.pdf`) // the rest of the patient's chart
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Mints a token exactly the way a real send does — get_templated_message is the endpoint the
|
|
100
|
+
* worker calls (worker/app.js handleOutgoingEmail).
|
|
101
|
+
*
|
|
102
|
+
* replaceLinks always rewrites the resolved link into a tracking redirect, so `plaintext` holds
|
|
103
|
+
* `doc (${API_URL}/r/<redirectToken>)` rather than the portal URL. Follow the 302 to recover it.
|
|
104
|
+
*/
|
|
105
|
+
const mint = async (verb: 'link' | 'shared-link') => {
|
|
106
|
+
const { plaintext } = await sdk.api.templates.get_templated_message({
|
|
107
|
+
channel: 'Email',
|
|
108
|
+
userId: tenant!.users[0].id,
|
|
109
|
+
enduserId: enduser.id,
|
|
110
|
+
message: `{{files.${fileA.id}.${verb}:doc}}`,
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
const redirectURL = plaintext.match(/\((https?:\/\/[^)\s]+)\)/)?.[1]
|
|
114
|
+
if (!redirectURL) return { token: undefined, detail: `no link resolved from: ${plaintext}` }
|
|
115
|
+
|
|
116
|
+
const r = await axios.get(redirectURL, { maxRedirects: 0, validateStatus: () => true })
|
|
117
|
+
const location = r.headers['location']
|
|
118
|
+
if (!location) return { token: undefined, detail: `redirect did not 302 (${r.status})` }
|
|
119
|
+
|
|
120
|
+
const token = new URL(location).searchParams.get('token') ?? undefined
|
|
121
|
+
return { token, detail: token ? 'minted' : `no token in ${location}` }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* ================================ confinement + the pin ================================ */
|
|
125
|
+
|
|
126
|
+
// Positive control first: the admin session reads both files fine, so a 403 below is confinement.
|
|
127
|
+
const adminFiles = await probe('get', '/files', { token: tenant.users[0].authToken })
|
|
128
|
+
record(
|
|
129
|
+
'control: admin session lists files',
|
|
130
|
+
adminFiles.status === 200, `status ${adminFiles.status}`,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
const confinement_probes = async (variant: 'link' | 'shared-link', token: string) => {
|
|
134
|
+
const p = (label: string, expected: number, r: Probe) => record(
|
|
135
|
+
`${variant} — ${label}`, r.status === expected, `status ${r.status} (expected ${expected})`,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
p('1: downloads the shared file', 200,
|
|
139
|
+
await probe('get', '/file-download-URL', { token, params: { secureName: fileA.secureName } }))
|
|
140
|
+
|
|
141
|
+
p('2: cannot download another file on the same patient', 403,
|
|
142
|
+
await probe('get', '/file-download-URL', { token, params: { secureName: fileB.secureName } }))
|
|
143
|
+
|
|
144
|
+
// validateInput merges query, then body, then params — body wins over query, so a pin checked
|
|
145
|
+
// against req.query rather than req.validated would be sidesteppable exactly like this.
|
|
146
|
+
p('3: cannot smuggle another secureName through the body', 403,
|
|
147
|
+
await probe('get', '/file-download-URL', {
|
|
148
|
+
token, params: { secureName: fileA.secureName }, data: { secureName: fileB.secureName },
|
|
149
|
+
}))
|
|
150
|
+
|
|
151
|
+
// a listing would turn the pin into a directory of the patient's chart
|
|
152
|
+
p('4: cannot list files', 403, await probe('get', '/files', { token }))
|
|
153
|
+
|
|
154
|
+
p('5a: cannot read chats', 403, await probe('get', '/chats', { token }))
|
|
155
|
+
p('5b: cannot read form responses', 403, await probe('get', '/form-responses', { token }))
|
|
156
|
+
p('5c: cannot read observations', 403, await probe('get', '/enduser-observations', { token }))
|
|
157
|
+
|
|
158
|
+
p('6: cannot update the patient', 403,
|
|
159
|
+
await probe('patch', `/enduser/${enduser.id}`, { token, data: { updates: { fname: 'Overwritten' } } }))
|
|
160
|
+
|
|
161
|
+
const refreshed = await probe('post', '/refresh-enduser-session', { token })
|
|
162
|
+
p('7: can refresh its own session', 200, refreshed)
|
|
163
|
+
|
|
164
|
+
// 7b — the refresh payload must be the fromPublicSession shape, not the whole chart
|
|
165
|
+
const leaked = ['dateOfBirth', 'email', 'phone', 'fields', 'insurance', 'lname']
|
|
166
|
+
.filter(f => (refreshed.body?.enduser ?? {})[f] !== undefined)
|
|
167
|
+
record(
|
|
168
|
+
`${variant} — 7b: refresh response withholds patient fields`,
|
|
169
|
+
refreshed.status === 200 && leaked.length === 0,
|
|
170
|
+
leaked.length ? `leaked: ${leaked.join(', ')}` : 'no patient fields in payload',
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
// 8 — confinement has to survive the re-mint, or one refresh launders it away
|
|
174
|
+
const refreshedToken = refreshed.body?.authToken
|
|
175
|
+
if (typeof refreshedToken === 'string' && refreshedToken) {
|
|
176
|
+
p('8a: refreshed token still cannot download another file', 403,
|
|
177
|
+
await probe('get', '/file-download-URL', {
|
|
178
|
+
token: refreshedToken, params: { secureName: fileB.secureName },
|
|
179
|
+
}))
|
|
180
|
+
p('8b: refreshed token still cannot update the patient', 403,
|
|
181
|
+
await probe('patch', `/enduser/${enduser.id}`, {
|
|
182
|
+
token: refreshedToken, data: { updates: { fname: 'Overwritten' } },
|
|
183
|
+
}))
|
|
184
|
+
} else {
|
|
185
|
+
record(`${variant} — 8: refreshed token available`, false, 'refresh returned no authToken')
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const plain = await mint('link')
|
|
190
|
+
if (plain.token) {
|
|
191
|
+
await confinement_probes('link', plain.token)
|
|
192
|
+
} else {
|
|
193
|
+
record('link — minted', false, plain.detail)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const shared = await mint('shared-link')
|
|
197
|
+
if (shared.token) {
|
|
198
|
+
await confinement_probes('shared-link', shared.token)
|
|
199
|
+
} else {
|
|
200
|
+
// Before the shared-link verb exists the merge field does not resolve. Record it and skip the
|
|
201
|
+
// shared probes rather than throwing, so the first run reports the whole picture at once.
|
|
202
|
+
record('shared-link — minted', false, shared.detail)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* ========================================= OTP ========================================= */
|
|
206
|
+
|
|
207
|
+
if (shared.token) {
|
|
208
|
+
const methods = await probe('get', '/endusers/otp-methods', { params: { token: shared.token } })
|
|
209
|
+
record(
|
|
210
|
+
'shared-link — 9: OTP methods are not offered',
|
|
211
|
+
methods.status === 403, `status ${methods.status} (expected 403)`,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
const send = await probe('post', '/endusers/send-otp-code', {
|
|
215
|
+
data: { token: shared.token, method: 'email' },
|
|
216
|
+
})
|
|
217
|
+
record(
|
|
218
|
+
'shared-link — 10: cannot make us send the patient a login code',
|
|
219
|
+
send.status === 403, `status ${send.status} (expected 403)`,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
// rejected on scope BEFORE the code lookup, so it is not an oracle on code validity
|
|
223
|
+
const verify = await probe('post', '/endusers/verify-otp-code', {
|
|
224
|
+
data: { token: shared.token, code: '000000' },
|
|
225
|
+
})
|
|
226
|
+
record(
|
|
227
|
+
'shared-link — 11: OTP verification refuses the scope before checking the code',
|
|
228
|
+
verify.status === 403, `status ${verify.status} (expected 403, 400 = reached the code check)`,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
// 12 — the same call with the scope claim stripped and the signature broken. The OTP endpoints
|
|
232
|
+
// jwtDecode without verifying, so without signature verification this sails straight through
|
|
233
|
+
// and every check above is decorative.
|
|
234
|
+
const [header, payload] = shared.token.split('.')
|
|
235
|
+
const decoded = JSON.parse(Buffer.from(payload, 'base64url').toString())
|
|
236
|
+
delete decoded.sessionScopes
|
|
237
|
+
delete decoded.scopeContext
|
|
238
|
+
const forged = [
|
|
239
|
+
header,
|
|
240
|
+
Buffer.from(JSON.stringify(decoded)).toString('base64url'),
|
|
241
|
+
'not-a-real-signature',
|
|
242
|
+
].join('.')
|
|
243
|
+
|
|
244
|
+
const forgedSend = await probe('post', '/endusers/send-otp-code', {
|
|
245
|
+
data: { token: forged, method: 'email' },
|
|
246
|
+
})
|
|
247
|
+
record(
|
|
248
|
+
'shared-link — 12: a forged token cannot strip the scope to unlock OTP',
|
|
249
|
+
forgedSend.status === 403 || forgedSend.status === 401 || forgedSend.status === 404,
|
|
250
|
+
`status ${forgedSend.status} (expected 401/403/404; 204 = signature never verified)`,
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (plain.token) {
|
|
255
|
+
// 13 — the patient-facing variant must keep its recovery path. Deliverable to a dontsend-
|
|
256
|
+
// address, so nothing leaves the system.
|
|
257
|
+
const send = await probe('post', '/endusers/send-otp-code', {
|
|
258
|
+
data: { token: plain.token, method: 'email' },
|
|
259
|
+
})
|
|
260
|
+
record(
|
|
261
|
+
'link — 13: patient OTP recovery still works',
|
|
262
|
+
send.status === 204, `status ${send.status} (expected 204)`,
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
} finally {
|
|
266
|
+
// Teardown BEFORE anything asserts — assert calls process.exit, which skips pending finallys and
|
|
267
|
+
// would strand the provisioned tenant permanently.
|
|
268
|
+
if (tenant) { try { await tenant.teardown() } catch (e) { console.error('teardown failed', e) } }
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return checks
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Asserting wrapper, for standalone runs and for sequential use in the suite. */
|
|
275
|
+
export const file_link_session_confinement_tests = async () => {
|
|
276
|
+
assert_checks('Shared file link session confinement', await file_link_session_confinement_checks())
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Allow running this test file independently
|
|
280
|
+
if (require.main === module) {
|
|
281
|
+
console.log(`🌐 Using API URL: ${host}`)
|
|
282
|
+
file_link_session_confinement_tests()
|
|
283
|
+
.then(() => {
|
|
284
|
+
console.log("✅ file link session confinement test suite completed successfully")
|
|
285
|
+
process.exit(0)
|
|
286
|
+
})
|
|
287
|
+
.catch((error) => {
|
|
288
|
+
console.error("❌ file link session confinement test suite failed:", error)
|
|
289
|
+
process.exit(1)
|
|
290
|
+
})
|
|
291
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
require('source-map-support').install();
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
assert_checks,
|
|
5
|
+
Check,
|
|
6
|
+
setup_isolated_tenant,
|
|
7
|
+
} from "../../setup_isolated"
|
|
8
|
+
|
|
9
|
+
const host = process.env.API_URL || 'http://localhost:8080' as const
|
|
10
|
+
|
|
11
|
+
const RAND = () => Math.random().toString(36).slice(2, 10)
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Regression test: `$JS(...)` must be rejected in stored conditional logic.
|
|
15
|
+
*
|
|
16
|
+
* `responses_satisfy_conditions` (utilities/src/utils.ts) compiled a stored condition value of the
|
|
17
|
+
* form `$JS(...)` with `new Function()` and executed it — in every form-filler's browser, including
|
|
18
|
+
* anonymous visitors on public intake pages, AND inside the API process (automation_triggers.ts
|
|
19
|
+
* trigger matching on every form submission, ai_agent/form_filling.ts voice-agent navigation). The
|
|
20
|
+
* API process holds every tenant's database handle, so a condition authored by staff in one tenant
|
|
21
|
+
* reached all of them. Any staff user or API key with form_fields write access could plant one by
|
|
22
|
+
* typing into the free-text $gt/$lt box — no API call required.
|
|
23
|
+
*
|
|
24
|
+
* The sink is removed. This asserts the second half of the fix: `compoundFilterValidator` rejects the
|
|
25
|
+
* syntax at write time on every field that feeds that evaluator, so it cannot be reintroduced through
|
|
26
|
+
* stored configuration. The runtime half — that a `$JS` value is never evaluated — is covered by the
|
|
27
|
+
* pure-function cases in `form_conditional_logic_tests` in tests.ts.
|
|
28
|
+
*
|
|
29
|
+
* The five condition locations were established by enumerating every `responses_satisfy_conditions`
|
|
30
|
+
* call site, and all five are covered here. That completeness matters: the first run of this test
|
|
31
|
+
* caught a half fix. `event.conditions` is wrapped in an `orValidator`, which returns the first arm
|
|
32
|
+
* that does not throw, and its permissive `optional: optionalAnyObjectValidator` arm was declared
|
|
33
|
+
* before the guarded one — so the payload was accepted and stored even though the validator had been
|
|
34
|
+
* swapped. Pointing a field at the guard is not the same as the guard being reachable, which is why
|
|
35
|
+
* every location gets an assertion rather than an argument.
|
|
36
|
+
*
|
|
37
|
+
* Each rejection is paired with a positive control using the same shape without `$JS(`, so a passing
|
|
38
|
+
* test cannot be explained by the write failing for an unrelated reason.
|
|
39
|
+
*/
|
|
40
|
+
export const form_logic_js_execution_checks = async (): Promise<Check[]> => {
|
|
41
|
+
const checks: Check[] = []
|
|
42
|
+
const record = (label: string, pass: boolean, detail: string) => checks.push({ label, pass, detail })
|
|
43
|
+
|
|
44
|
+
const JS_PAYLOAD = '$JS(return 0)'
|
|
45
|
+
const SAFE_VALUE = '18'
|
|
46
|
+
|
|
47
|
+
const condition = (value: string) => ({ $and: [{ condition: { 'someFieldId': { $gt: value } } }] })
|
|
48
|
+
const previousFields = (value: string) => ([{
|
|
49
|
+
type: 'compoundLogic',
|
|
50
|
+
info: { fieldId: '000000000000000000000000', priority: 5, label: '', condition: condition(value) },
|
|
51
|
+
}])
|
|
52
|
+
|
|
53
|
+
const attempt = async (fn: () => Promise<any>) => (
|
|
54
|
+
fn().then(() => ({ accepted: true, detail: 'accepted' }))
|
|
55
|
+
.catch((e: any) => ({ accepted: false, detail: `rejected: ${e?.message || e}` }))
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
let tenant: Awaited<ReturnType<typeof setup_isolated_tenant>> | undefined
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
tenant = await setup_isolated_tenant({ name: `F0200 ${RAND()}`, users: [{ isAdmin: true }], apiKeys: 0 })
|
|
62
|
+
const sdk = tenant.sdk
|
|
63
|
+
|
|
64
|
+
const form = await sdk.api.forms.createOne({ title: `${RAND()}` })
|
|
65
|
+
const field = await sdk.api.form_fields.createOne({
|
|
66
|
+
formId: form.id, title: 'anchor', type: 'string', previousFields: [{ type: 'root', info: {} }],
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
/* ===== 1. previousFields[].info.condition — the field the finding was reported against ===== */
|
|
70
|
+
|
|
71
|
+
const controlCreate = await attempt(() => sdk.api.form_fields.createOne({
|
|
72
|
+
formId: form.id, title: 'control', type: 'string',
|
|
73
|
+
previousFields: previousFields(SAFE_VALUE) as any,
|
|
74
|
+
}))
|
|
75
|
+
record(
|
|
76
|
+
'control: form_fields.createOne accepts a normal $gt condition',
|
|
77
|
+
controlCreate.accepted, controlCreate.detail,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
const payloadCreate = await attempt(() => sdk.api.form_fields.createOne({
|
|
81
|
+
formId: form.id, title: 'payload', type: 'string',
|
|
82
|
+
previousFields: previousFields(JS_PAYLOAD) as any,
|
|
83
|
+
}))
|
|
84
|
+
record(
|
|
85
|
+
'1a: form_fields.createOne rejects $JS( in previousFields condition',
|
|
86
|
+
!payloadCreate.accepted, payloadCreate.detail,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
const payloadUpdate = await attempt(() => sdk.api.form_fields.updateOne(field.id, {
|
|
90
|
+
previousFields: previousFields(JS_PAYLOAD) as any,
|
|
91
|
+
}))
|
|
92
|
+
record(
|
|
93
|
+
'1b: form_fields.updateOne rejects $JS( in previousFields condition',
|
|
94
|
+
!payloadUpdate.accepted, payloadUpdate.detail,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
/* ===== 2. groupShowCondition ===== */
|
|
98
|
+
|
|
99
|
+
const groupControl = await attempt(() => sdk.api.form_fields.updateOne(field.id, {
|
|
100
|
+
groupShowCondition: condition(SAFE_VALUE) as any,
|
|
101
|
+
}))
|
|
102
|
+
record('control: groupShowCondition accepts a normal condition', groupControl.accepted, groupControl.detail)
|
|
103
|
+
|
|
104
|
+
const groupPayload = await attempt(() => sdk.api.form_fields.updateOne(field.id, {
|
|
105
|
+
groupShowCondition: condition(JS_PAYLOAD) as any,
|
|
106
|
+
}))
|
|
107
|
+
record('2: form_fields.updateOne rejects $JS( in groupShowCondition', !groupPayload.accepted, groupPayload.detail)
|
|
108
|
+
|
|
109
|
+
/* ===== 3. options.optionDetails[].showCondition ===== */
|
|
110
|
+
|
|
111
|
+
const optionControl = await attempt(() => sdk.api.form_fields.updateOne(field.id, {
|
|
112
|
+
options: { optionDetails: [{ option: 'A', showCondition: condition(SAFE_VALUE) }] } as any,
|
|
113
|
+
}))
|
|
114
|
+
record('control: optionDetails showCondition accepts a normal condition', optionControl.accepted, optionControl.detail)
|
|
115
|
+
|
|
116
|
+
const optionPayload = await attempt(() => sdk.api.form_fields.updateOne(field.id, {
|
|
117
|
+
options: { optionDetails: [{ option: 'A', showCondition: condition(JS_PAYLOAD) }] } as any,
|
|
118
|
+
}))
|
|
119
|
+
record('3: form_fields.updateOne rejects $JS( in optionDetails showCondition', !optionPayload.accepted, optionPayload.detail)
|
|
120
|
+
|
|
121
|
+
/* ===== 4. options.productConditions[].showCondition ===== */
|
|
122
|
+
|
|
123
|
+
const productControl = await attempt(() => sdk.api.form_fields.updateOne(field.id, {
|
|
124
|
+
options: { productConditions: [{ productId: '000000000000000000000000', showCondition: condition(SAFE_VALUE) }] } as any,
|
|
125
|
+
}))
|
|
126
|
+
record('control: productConditions showCondition accepts a normal condition', productControl.accepted, productControl.detail)
|
|
127
|
+
|
|
128
|
+
const productPayload = await attempt(() => sdk.api.form_fields.updateOne(field.id, {
|
|
129
|
+
options: { productConditions: [{ productId: '000000000000000000000000', showCondition: condition(JS_PAYLOAD) }] } as any,
|
|
130
|
+
}))
|
|
131
|
+
record('4: form_fields.updateOne rejects $JS( in productConditions showCondition', !productPayload.accepted, productPayload.detail)
|
|
132
|
+
|
|
133
|
+
/* ===== 5. automation_triggers — the server-side evaluation path ===== */
|
|
134
|
+
|
|
135
|
+
const trigger = (conditions: object, extraInfo: object = {}) => ({
|
|
136
|
+
title: `${RAND()}`,
|
|
137
|
+
status: 'Inactive',
|
|
138
|
+
event: { type: 'Form Submitted', info: { formId: form.id, ...extraInfo }, ...conditions },
|
|
139
|
+
action: { type: 'Add Tags', info: { tags: ['security-regression'] } },
|
|
140
|
+
} as any)
|
|
141
|
+
|
|
142
|
+
const triggerControl = await attempt(() => sdk.api.automation_triggers.createOne(
|
|
143
|
+
trigger({ conditions: condition(SAFE_VALUE) })
|
|
144
|
+
))
|
|
145
|
+
record('control: automation_triggers accepts normal event conditions', triggerControl.accepted, triggerControl.detail)
|
|
146
|
+
|
|
147
|
+
// NOTE: this is the case that caught the half fix — the orValidator's permissive arm accepted it.
|
|
148
|
+
const triggerPayload = await attempt(() => sdk.api.automation_triggers.createOne(
|
|
149
|
+
trigger({ conditions: condition(JS_PAYLOAD) })
|
|
150
|
+
))
|
|
151
|
+
record('5a: automation_triggers rejects $JS( in event.conditions', !triggerPayload.accepted, triggerPayload.detail)
|
|
152
|
+
|
|
153
|
+
const byFormIdPayload = await attempt(() => sdk.api.automation_triggers.createOne(
|
|
154
|
+
trigger({}, { conditionsByFormId: { [form.id]: condition(JS_PAYLOAD) } })
|
|
155
|
+
))
|
|
156
|
+
record('5b: automation_triggers rejects $JS( in event.info.conditionsByFormId', !byFormIdPayload.accepted, byFormIdPayload.detail)
|
|
157
|
+
} finally {
|
|
158
|
+
// Teardown BEFORE anything asserts — assert calls process.exit, which skips pending finallys and
|
|
159
|
+
// would strand the provisioned tenant permanently.
|
|
160
|
+
if (tenant) { try { await tenant.teardown() } catch (e) { console.error('teardown failed', e) } }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return checks
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Asserting wrapper, for standalone runs and for sequential use in the suite. */
|
|
167
|
+
export const form_logic_js_execution_tests = async () => {
|
|
168
|
+
assert_checks('$JS in stored conditional logic', await form_logic_js_execution_checks())
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Allow running this test file independently
|
|
172
|
+
if (require.main === module) {
|
|
173
|
+
console.log(`🌐 Using API URL: ${host}`)
|
|
174
|
+
form_logic_js_execution_tests()
|
|
175
|
+
.then(() => {
|
|
176
|
+
console.log("✅ form logic JS execution test suite completed successfully")
|
|
177
|
+
process.exit(0)
|
|
178
|
+
})
|
|
179
|
+
.catch((error) => {
|
|
180
|
+
console.error("❌ form logic JS execution test suite failed:", error)
|
|
181
|
+
process.exit(1)
|
|
182
|
+
})
|
|
183
|
+
}
|
|
@@ -10,8 +10,7 @@ import { setup_tests } from "../../setup"
|
|
|
10
10
|
const host = process.env.API_URL || 'http://localhost:8080' as const
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Regression test
|
|
14
|
-
* (security-audit/findings/F-0145-globalunique-conflict-leaks-cross-tenant-record.md).
|
|
13
|
+
* Regression test: a globalUnique conflict response must not echo the matched record.
|
|
15
14
|
*
|
|
16
15
|
* The create endpoint generator's unconditional globalUnique-enforcement block
|
|
17
16
|
* (routing.ts, "globalUnique should be enforced even if _overrideUnique is true")
|
|
@@ -24,7 +23,7 @@ const host = process.env.API_URL || 'http://localhost:8080' as const
|
|
|
24
23
|
* 409 body's existingRecord exposes nothing beyond _id + businessId.
|
|
25
24
|
*/
|
|
26
25
|
export const globalunique_leak_tests = async ({ sdk } : { sdk: Session, sdkNonAdmin: Session }) => {
|
|
27
|
-
log_header("
|
|
26
|
+
log_header("globalUnique conflict response redaction")
|
|
28
27
|
|
|
29
28
|
let userId: string | undefined
|
|
30
29
|
try {
|
|
@@ -33,7 +32,7 @@ export const globalunique_leak_tests = async ({ sdk } : { sdk: Session, sdkNonAd
|
|
|
33
32
|
userId = user.id
|
|
34
33
|
|
|
35
34
|
await async_test(
|
|
36
|
-
'
|
|
35
|
+
'globalUnique 409 existingRecord exposes only _id + businessId (no email/hashedPass)',
|
|
37
36
|
() => sdk.api.users.createOne({ email, _overrideUnique: true } as any),
|
|
38
37
|
{
|
|
39
38
|
shouldError: true,
|
|
@@ -68,11 +67,11 @@ if (require.main === module) {
|
|
|
68
67
|
|
|
69
68
|
runTests()
|
|
70
69
|
.then(() => {
|
|
71
|
-
console.log("✅
|
|
70
|
+
console.log("✅ globalUnique leak test suite completed successfully")
|
|
72
71
|
process.exit(0)
|
|
73
72
|
})
|
|
74
73
|
.catch((error) => {
|
|
75
|
-
console.error("❌
|
|
74
|
+
console.error("❌ globalUnique leak test suite failed:", error)
|
|
76
75
|
process.exit(1)
|
|
77
76
|
})
|
|
78
77
|
}
|
|
@@ -27,7 +27,7 @@ const post = async (path: string, body: any, headers: Record<string, string> = {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
* Regression test
|
|
30
|
+
* Regression test: `handle_incoming_communication` tenant scoping.
|
|
31
31
|
*
|
|
32
32
|
* `journeys.handle_incoming_communication` previously used `buildAllQueries({ unrestricted: true, organizationIds: [] }).endusers.findById(enduserId)`,
|
|
33
33
|
* permitting cross-tenant lookup of any enduser by id. The handler then called `handleIncomingCommunication(...)`
|
|
@@ -45,7 +45,7 @@ const post = async (path: string, body: any, headers: Record<string, string> = {
|
|
|
45
45
|
* enduser — the post-fix code returns 404 before any side effects fire, and the assertion confirms that.
|
|
46
46
|
*/
|
|
47
47
|
export const handle_incoming_communication_cross_tenant_tests = async ({ sdk, sdkNonAdmin } : { sdk: Session, sdkNonAdmin: Session }) => {
|
|
48
|
-
log_header("
|
|
48
|
+
log_header("handle_incoming_communication cross-tenant rejection")
|
|
49
49
|
|
|
50
50
|
// ====================================================================
|
|
51
51
|
// Assertion 1: nonexistent enduserId returns 404 (baseline; regression
|
|
@@ -58,19 +58,19 @@ export const handle_incoming_communication_cross_tenant_tests = async ({ sdk, sd
|
|
|
58
58
|
{ Authorization: `Bearer ${sdk.authToken}` },
|
|
59
59
|
)
|
|
60
60
|
await async_test(
|
|
61
|
-
"
|
|
61
|
+
"handle_incoming_communication with nonexistent enduserId returns 404",
|
|
62
62
|
async () => ({ status: nonexistentRes.status }),
|
|
63
63
|
{ onResult: r => r.status === 404 },
|
|
64
64
|
)
|
|
65
65
|
|
|
66
66
|
// ====================================================================
|
|
67
|
-
// Assertion 2: cross-tenant enduserId returns 404 (the actual
|
|
67
|
+
// Assertion 2: cross-tenant enduserId returns 404 (the actual fix).
|
|
68
68
|
// Env-gated; skipped when cross-org infra isn't configured.
|
|
69
69
|
// Safe to run post-fix because the tenant-scoped DB returns null for
|
|
70
70
|
// cross-tenant lookups — no handleIncomingCommunication side effect fires.
|
|
71
71
|
// ====================================================================
|
|
72
72
|
if (!(CROSS_ORG_API_KEY && CROSS_ORG_TARGET_BUSINESS_ID)) {
|
|
73
|
-
console.log("
|
|
73
|
+
console.log(" Skipping cross-tenant rejection assertion — CROSS_ORG_* env vars not set")
|
|
74
74
|
return
|
|
75
75
|
}
|
|
76
76
|
|
|
@@ -96,7 +96,7 @@ export const handle_incoming_communication_cross_tenant_tests = async ({ sdk, sd
|
|
|
96
96
|
)
|
|
97
97
|
|
|
98
98
|
await async_test(
|
|
99
|
-
"
|
|
99
|
+
"handle_incoming_communication with cross-tenant enduserId returns 404 (no side effect)",
|
|
100
100
|
async () => ({
|
|
101
101
|
status: crossRes.status,
|
|
102
102
|
message: crossRes.data?.message ?? null,
|
|
@@ -120,11 +120,11 @@ if (require.main === module) {
|
|
|
120
120
|
|
|
121
121
|
runTests()
|
|
122
122
|
.then(() => {
|
|
123
|
-
console.log("✅
|
|
123
|
+
console.log("✅ handle_incoming_communication cross-tenant test suite completed successfully")
|
|
124
124
|
process.exit(0)
|
|
125
125
|
})
|
|
126
126
|
.catch((error) => {
|
|
127
|
-
console.error("❌
|
|
127
|
+
console.error("❌ handle_incoming_communication cross-tenant test suite failed:", error)
|
|
128
128
|
process.exit(1)
|
|
129
129
|
})
|
|
130
130
|
}
|
|
@@ -27,7 +27,7 @@ const post = async (path: string, body: any, headers: Record<string, string> = {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
* Regression test
|
|
30
|
+
* Regression test: `invite_user` cross-tenant enumeration.
|
|
31
31
|
*
|
|
32
32
|
* `users.invite_user` previously used `buildAllQueries({ unrestricted: true, organizationIds: [] }).users.findOne({ email })`
|
|
33
33
|
* to enforce platform-wide email uniqueness and threw the distinctive `"A user with this email already exists"`
|
|
@@ -50,7 +50,7 @@ const post = async (path: string, body: any, headers: Record<string, string> = {
|
|
|
50
50
|
* env vars are not set, mirroring cross_org_api_key.test.ts convention.
|
|
51
51
|
*/
|
|
52
52
|
export const invite_user_enumeration_tests = async ({ sdk, sdkNonAdmin } : { sdk: Session, sdkNonAdmin: Session }) => {
|
|
53
|
-
log_header("
|
|
53
|
+
log_header("users.invite_user cross-tenant enumeration regression")
|
|
54
54
|
|
|
55
55
|
// Reset state so prior tests' rate-limit accounting doesn't leak in.
|
|
56
56
|
await sdk.reset_db()
|
|
@@ -85,7 +85,7 @@ export const invite_user_enumeration_tests = async ({ sdk, sdkNonAdmin } : { sdk
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
await async_test(
|
|
88
|
-
"
|
|
88
|
+
"invite_user rate-limits within ~12 rapid requests (defense-in-depth; no invites sent)",
|
|
89
89
|
async () => ({ rateLimitedAt }),
|
|
90
90
|
{ onResult: r => r.rateLimitedAt >= 0 && r.rateLimitedAt <= 12 },
|
|
91
91
|
)
|
|
@@ -111,7 +111,7 @@ export const invite_user_enumeration_tests = async ({ sdk, sdkNonAdmin } : { sdk
|
|
|
111
111
|
)
|
|
112
112
|
|
|
113
113
|
await async_test(
|
|
114
|
-
"
|
|
114
|
+
"invite_user same-tenant duplicate returns 400 'already exists' (unchanged)",
|
|
115
115
|
async () => ({
|
|
116
116
|
status: sameTenantRes.status,
|
|
117
117
|
message: sameTenantRes.data?.message ?? '',
|
|
@@ -124,7 +124,7 @@ export const invite_user_enumeration_tests = async ({ sdk, sdkNonAdmin } : { sdk
|
|
|
124
124
|
// Env-gated; skipped when cross-org infra not configured.
|
|
125
125
|
// ====================================================================
|
|
126
126
|
if (!(CROSS_ORG_API_KEY && CROSS_ORG_TARGET_BUSINESS_ID)) {
|
|
127
|
-
console.log("
|
|
127
|
+
console.log(" Skipping cross-tenant silent no-op assertion — CROSS_ORG_* env vars not set")
|
|
128
128
|
return
|
|
129
129
|
}
|
|
130
130
|
|
|
@@ -160,7 +160,7 @@ export const invite_user_enumeration_tests = async ({ sdk, sdkNonAdmin } : { sdk
|
|
|
160
160
|
)
|
|
161
161
|
|
|
162
162
|
await async_test(
|
|
163
|
-
"
|
|
163
|
+
"cross-tenant invite of existing email must NOT return 'already exists' (no enumeration)",
|
|
164
164
|
async () => ({
|
|
165
165
|
status: crossRes.status,
|
|
166
166
|
message: crossRes.data?.message ?? '',
|
|
@@ -188,11 +188,11 @@ if (require.main === module) {
|
|
|
188
188
|
|
|
189
189
|
runTests()
|
|
190
190
|
.then(() => {
|
|
191
|
-
console.log("✅
|
|
191
|
+
console.log("✅ invite_user enumeration test suite completed successfully")
|
|
192
192
|
process.exit(0)
|
|
193
193
|
})
|
|
194
194
|
.catch((error) => {
|
|
195
|
-
console.error("❌
|
|
195
|
+
console.error("❌ invite_user enumeration test suite failed:", error)
|
|
196
196
|
process.exit(1)
|
|
197
197
|
})
|
|
198
198
|
}
|