Conversation
…hare links + plan signups
…sed, tenant scoping; journal entry for 0023
… replay/conflict reads)
|
Verification fixes pushed — addressing the tenant-scoping bug and the 0023 migration collision. 1. Tenant-scoped
|
…ques); branch hygiene: verbatim main journal + idx-24 append + 0023_subject_portal.sql
…itlement uniques enforced in fake)
|
Re-verification fixes pushed — both remaining items addressed. 1. Free-plan key-squatting bug (probe-verified) — FIXEDRoot cause: Fix in
with a code comment citing the two global constraints. Paid plans are unaffected (billing ref is the globally-unique server-created Regression test 2. Branch hygiene — mergeable again
so the branch diff vs main is purely additive and the Real test output (re-run after fixes)Commits: |
|
Superseded by #157 ( |
What
WP4 — shareable investigation reports + self-service subscriptions (Intelius instant-report + self-serve pricing analogs).
server/shareableReports.ts(new,shareableReportsRouter):createShareLink(writeProcedure): verifies the investigation belongs toctx.tenantId(FOR SHAREinside a tx); issues abis_sl_<random>token shown exactly once — only its SHA-256 hex digest is persisted (same scheme asapiTokens/openclawEndpointsbearer validation); expiry defaults to 7 days, validated max 30; writes an HMAC-chainedaudit_logrow + publishesREPORT_SHARE_CREATED.getSharedReport(token-authed publicProcedure): hash lookup with unexpired + unrevoked enforced in a single atomicUPDATE … RETURNINGthat also incrementsview_count/last_viewed_at(revoked/expired links cannot be raced into extra views). Returns a whitelisted redacted one-pager: subject name, investigation ref, overall risk band (derived from tier/score — raw scores never serialised), per-source screening outcomes reduced to pass/consider/fail (fromscreening_resultsviascreening_orders, tenant-scoped), field-visit outcome, thin-file flag, generated-at, tenant display name. Referee identities, raw payloads, internal notes, and user IDs are never selected.revokeShareLink/listShareLinks: tenant-scoped;listShareLinksnever selectstoken_hash.server/selfServiceBilling.ts(new,selfServiceBillingRouter):listPublicPlans(public query): read-only projection of the existingbilling_planscatalogue.signup(writeProcedure): client-supplied idempotency key backed by a per-tenant UNIQUE constraintplan_signups.(tenant_id, idempotency_key)— a same-tenant replay returns the original result withidempotent: trueand never re-settles payment; a same-tenant concurrent race loses on 23505 and is re-read (tenant-scoped) as the original. A key belonging to another tenant is invisible and behaves as a new key for this tenant — replays can never leak a foreign signupId/billingRef, and tenants cannot squat each other's keys (this holds for zero-price plans too: the syntheticprovider_subscription_ref/source_referenceare tenant-namespaced asself-serve-free:<tenantId>:<key>/self-serve-signup:<tenantId>:<key>becausetenant_subscriptionsandbilling_entitlementsenforce GLOBAL uniques). Plan resolved frombilling_plans(active only). Payment goes exclusively through the existingsettlePaystackPayment(server/billingSettlement.ts): thebilling_payment_intentsrow must be server-created, tenant-bound,purpose='subscription_invoice', and amount-equal to the plan price before settling; settlement re-verifies with Paystack and posts the deterministic TigerBeetle transfer. Activation mirrorsactivateManualContractinternals transactionally (cancel current sub → inserttenant_subscriptionsactive → grantbilling_entitlementsincluded checks → insertplan_signups). Fail-closed: any payment/ledger failure → typed TRPCError, zero subscription/entitlement, attempt durably recorded asplan_signups.status='payment_failed'+ failure audit row. Audit +PLAN_SIGNUP_ACTIVATEDevent on success.mySubscription,usageSummary: tenant-scoped reads oftenant_subscriptions⋈billing_plansandbilling_entitlements/billing_usage_events.drizzle/0024_share_links_and_plan_signups.sql(new) + journal entry idx 24; branch carries main'sdrizzle/0023_subject_portal.sqlunchanged and main's_journal.jsonverbatim + the idx-24 entry appended, so the diff vs main is purely additive (no merge conflict).plan_signupsuniqueness isUNIQUE (tenant_id, idempotency_key). Raw-SQL-only pattern (like the informal_verification tables — applied bypnpm db:migrate).Why
Closes the WP4 gap: no way to share a redacted investigation result with an external party, and no self-service path onto a commercial plan (today only admin-run
activateManualContract).How tested
pnpm install(pnpm 10.27.0), then:19 tests cover: token lifecycle (create → view ×2 counted atomically → revoke → rejected; expired rejected; unknown rejected; 30-day cap), redaction shape (exact whitelisted object equality + recursive forbidden-key scan over
riskScore|rawResult|referee|notes|createdBy|userId|agentId|token_hash|nin|bvn|…+ serialized substring checks), same-tenant idempotency replay (same signupId/subscriptionId, Paystack verify + TigerBeetle transfer called exactly once), cross-tenant idempotency-key replay non-leakage (tenant 2 presenting tenant 1's key gets a brand-new signup — distinct signupId/subscriptionId/billingRef, serialized output contains none of tenant 1's identifiers — plus an SQL-level guard asserting everyplan_signupsreplay lookup carriestenant_id = $1 AND idempotency_key = $2), free-plan cross-tenant same-key coexistence (both tenants activate independently with tenant-namespaced refs; fake enforces the real GLOBAL uniques ontenant_subscriptions.provider_subscription_refandbilling_entitlements.source_reference), fail-closed payment failure (no sub/entitlement,payment_failedrecorded, replay returns original failure without re-settling), cross-tenant denial for share create/revoke/intent binding, and tenant-scopedmySubscription/usageSummary. Tests drive the real routers viacreateCallerwith a stateful in-memory pg handler executing the production SQL (incl.settlePaystackPayment's queries); only the external HTTP boundaries (Paystack verify, TigerBeetle, event processor) are intercepted via stubbedfetch.Regression:
billing.test.ts,billing.debitClaim.test.ts,billing.topup.idempotency.test.ts,paymentReconciliation.test.ts,smoke.comprehensive.test.ts→ 131/132 pass. The 1 failure (smoke.comprehensive›creditTenantAccount rejects an unbound legacy reference when TIGERBEETLE_URL is not set) is pre-existing on pristine main (verified against a clean main extract) and is unrelated to this change.Integration patches (large files, applied by orchestrator)
server/routers.tsanddrizzle/schema.tsare too large for MCP push; apply these exactly (verified locally with typecheck + tests):1.
server/routers.ts— imports. Anchor (exists at ~line 142):Insert immediately after it:
2.
server/routers.ts— registration. Anchor at the END of theappRouterobject (~line 7691):Change to:
3.
drizzle/schema.ts— append at END of file (optional Drizzle types; runtime code uses raw SQL against the 0024 tables, so this is for type consumers only). Anchor: the final two lines are theInsertForceCreditApprovaltype exports. Append after them:Risks
signuprequires a pre-createdbilling_payment_intentsrow withpurpose='subscription_invoice'(create it via the existingstartPaystackTopup({..., purpose: 'subscription_invoice'})— exported in billingSettlement.ts). Zero-price plans activate without payment (provider='manual_contract',billing_ref='self-serve-free:<tenantId>:<key>').payment_failedidempotency key is terminal by design (replay returns the original failure); a genuinely new attempt uses a new key — standard idempotency semantics. Keys are tenant-namespaced, so the same key string in another tenant is an independent signup.getSharedReportis deliberately unauthenticated (bearer-token-in-URL model); the token is 192 bits of entropy, hashed at rest, expiring, revocable.workflowscope); runpnpm vitest run server/share-subscribe.test.tsin existing test jobs.