@odla-ai/chapter 0.47.10 → 0.48.1

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/README.md CHANGED
@@ -88,13 +88,13 @@ Chapter-owned application, admin, CRM, payment, and scheduling behavior.
88
88
  *or* an odla-db `admins` allowlist, plus a read-only `superAdmins` tier —
89
89
  selected by `auth.source`, defaulting per mode. Escalation guards
90
90
  (`canChangeRole`) are package-enforced.
91
- - **Correctness is packaged, not per-site.** Replay-deduplicated email
92
- (`sendTemplated`/`isAlreadySent`; concurrent sends still need a serialized
93
- outbox/provider idempotency for a true exactly-once guarantee), a non-prod
91
+ - **Correctness is packaged, not per-site.** Durably claimed email
92
+ (`sendTemplated`/`isAlreadySent`; ambiguous non-idempotent provider outcomes
93
+ require evidence-based reconciliation rather than blind resend), a non-prod
94
94
  delivery fail-safe (`planDelivery`), stable application replay identity (one
95
95
  UI submission key and the same resumable canonical id), status-never-backwards
96
96
  (`canTransition`), Stripe webhook integrity (`verifyStripeSignature`) with the
97
- webhook as the authoritative writer of paid/refunded,
97
+ verified Stripe events and refund reconciliation as the payment authority,
98
98
  one-subscription-per-application idempotency, meetings-as-canonical booking (a
99
99
  rebooking *reschedules* the event, preserving the Meet link), Google-edit
100
100
  adoption (`reconcileMeetings`), and a one-way CRM projection.
@@ -381,6 +381,12 @@ The same renderer can also replace the placeholder chooser inside
381
381
  `redirect_status` and `reschedule` values never choose a UI step. Supply
382
382
  `loadResume` when a host stores the capability elsewhere.
383
383
 
384
+ Closed/refunded/declined applications return a scoped `409` with
385
+ `code: "application_closed"`; `loadJoinResume` maps this to `step: "closed"`.
386
+ The island shows a terminal notice without another form, checkout, or booking.
387
+ Unknown continuation states and mismatched application ids are rejected rather
388
+ than defaulting to payment. Temporary configuration failures remain retryable.
389
+
384
390
  Studio and automated previews can traverse that same state machine without
385
391
  creating an application, loading Stripe, charging a card, or booking a real
386
392
  calendar event. Supply deterministic `simulation` boundaries and keep the
@@ -609,6 +615,24 @@ The hub builds a complete `SignupControlDocument`, calls
609
615
  secret is vaulted separately on each side; it is never a Clerk token, ODLA key,
610
616
  or CRM network secret.
611
617
 
618
+ For multiple authoritative runtimes sharing a data environment, set
619
+ `signupControl.runtimeSecrets` to an explicit map such as
620
+ `{ cory: "signup_control_cory", tori: "signup_control_tori" }`. Each value names
621
+ a different follower vault key; paired hub credentials remain separate too.
622
+ Unlisted runtimes and untargeted legacy deliveries then fail closed. Include
623
+ `deliveryTarget: { appId, environment, runtime }` beside the envelope's
624
+ `document` and `digest`. This transport identity is signed but excluded from
625
+ the document digest, so both runtimes accept or replay the same reviewed state.
626
+ The receipt attests the receiving runtime; the sender validates exact target,
627
+ revision, digest, status, and timestamp. Delivery does not follow redirects and
628
+ bounds receipt reads to 8 KiB and ten seconds.
629
+
630
+ Provision the additive `signupControlHeads` namespace before deploying this
631
+ receiver. Its transaction guard serializes concurrent first publications and
632
+ stale work across runtimes. The hub must likewise allocate revisions atomically
633
+ and freeze the document in the same transaction as its per-runtime outbox rows.
634
+ No process should regenerate an old revision from the current catalog on retry.
635
+
612
636
  The document carries the source, exact chapter/environment/Stripe-mode target,
613
637
  monotonic revision, ordered and available tiers, immutable Stripe Price ids,
614
638
  the membership-page renderer document, and explicitly delegated application,
@@ -629,6 +653,12 @@ join-config, application tier validation, and checkout already read. Public
629
653
  application contract, and theme. Applicant requests therefore use local
630
654
  last-known-good state and never depend on the hub being reachable.
631
655
 
656
+ A successful empty tier query retains legacy single-tier compatibility.
657
+ Database failures, malformed responses, and missing stored prices fail closed;
658
+ they never invent a free membership. Retiring all stored tiers leaves no offers
659
+ instead of reviving the group's legacy price. Public Worker routes return
660
+ `503` with `tiers_unavailable` for this failure rather than charging a fallback.
661
+
632
662
  #### One field contract for follower forms and CMS management
633
663
 
634
664
  Chapter owns field identity and storage authority; a hub such as Built Not
@@ -1502,18 +1532,141 @@ These bite silently — a smoke test won't catch them:
1502
1532
  method, auth, request, response shape, units, status, and header parity are
1503
1533
  proven. Equal values with different JSON contracts are not compatible. For a
1504
1534
  join flow, adopt `JoinIsland` end to end or keep an explicit tested adapter;
1505
- do not merely repoint an existing page at `/api/join-config`. `PaymentStep`
1535
+ do not merely repoint an existing page at `/api/join-config`. `JoinIsland`
1536
+ now posts to `/api/payments/quote?application=<id>` before exposing
1537
+ membership payment consent. The quote uses the application's recorded tier,
1538
+ not a URL selection, and includes its amount, currency, policy, and digest.
1539
+ Optional signed `policy.tierPolicies` overrides supply refund/trust copy for
1540
+ declared tier IDs. Checkout validates a supplied quote digest before provider
1541
+ writes; a changed quote must be shown and accepted again. The accepted quote
1542
+ is frozen with the checkout intent so later catalog edits cannot silently
1543
+ change an in-progress purchase. Hosts upgrading the complete join client can
1544
+ set `chapterWorker({ chapter, requirePaymentQuote: true })` to reject older
1545
+ requests without a digest. The default preserves old published clients; new
1546
+ membership `JoinIsland` clients always send the digest. Gift purchase and inert
1547
+ simulation flows retain their separate contracts. The standalone `PaymentStep`
1506
1548
  obtains Stripe's `clientSecret`, `publishableKey`, and `lineItems` from
1507
1549
  `POST /api/payments/subscription`, not from the public join-config response.
1550
+ Subscription setup verifies Stripe's immutable amount, currency, recurrence,
1551
+ quantity model, and test/live mode before creating a customer or subscription.
1552
+ A saved subscription is retrieved by ID on replay and checked against the
1553
+ customer, runtime partition, original invoice, and Price; it is never recreated.
1554
+ An ambiguous create with no saved provider ID stops after 23 hours with
1555
+ `checkout_recovery_required`, before Stripe can prune its idempotency key.
1556
+ Reconcile that exact purchase before retrying; do not clear its intent to force
1557
+ another charge.
1558
+ For intent-backed first invoices, the signed webhook also verifies the exact
1559
+ subscription, customer, paid amount, currency, and invoice status before
1560
+ advancing membership or applying an interview waiver. A webhook arriving
1561
+ before the checkout-ready write returns a retryable response. Mismatched
1562
+ evidence is quarantined without sending payment notices or granting access.
1563
+ Renewal dates come from an unambiguous recurring invoice line, not whichever
1564
+ line Stripe happens to list first; manual and proration/update invoices do not
1565
+ renew membership.
1566
+
1567
+ The internal provider adapter can freeze separate, versioned percentage-coupon
1568
+ terms alongside the base recurring Price. An optional extended initial term
1569
+ charges a full up-front invoice item and defers recurring billing to the frozen
1570
+ service end. This uses Stripe's trial anchor mechanically, **not** a free
1571
+ membership: access still requires the verified paid invoice. The coupon must
1572
+ match the exact app/environment/runtime and policy version; a missing coupon
1573
+ is provisioned idempotently and restricted to the verified membership product,
1574
+ so it cannot also discount a named seat.
1575
+ Only first-invoice and continuing coupons are supported; arbitrary timed
1576
+ discount expiry is rejected rather than silently becoming permanent. These
1577
+ adapter terms are not a public join-config override. A host may opt into
1578
+ `chapterWorker({ chapter, membershipAuthority: adapter.authority })`, where
1579
+ `adapter = createMembershipWorkerAdapter({ chapterId, secretName, makeDb,
1580
+ targetUrl, fetch, namedSeats })` comes from `@odla-ai/chapter/worker`.
1581
+ The factory wires payment, approval, refund, renewal, current access, and
1582
+ optional named-seat operations through the same exact runtime edge. `fetch`
1583
+ and `targetUrl` resolve from the host environment; an absent configured
1584
+ binding fails closed and never falls back to a different deployment.
1585
+ All callbacks must target the same network authority scope, never a local
1586
+ shadow ledger. They may use `requestMembershipCommercialOffer` and
1587
+ `requestMembershipCheckoutBinding`, `requestMembershipPayment` and
1588
+ `requestMembershipApproval` against an authenticated existing service,
1589
+ or invoke the corresponding network commands in process. This API does not
1590
+ require or provision a new app or Worker.
1591
+
1592
+ With that adapter configured, POST prepares a durable offer projection before
1593
+ returning consent terms; GET only reads an existing quote. A lost issuer reply
1594
+ reuses the persisted request identity. An unexpired quote survives later
1595
+ catalog changes. An expired quote can be replaced only before a checkout
1596
+ intent exists; changed terms require fresh consent. The base chapter Price
1597
+ must match the authority offering's amount, currency and recurrence.
1598
+ After consent, a durable network binding fixes the application, offer, Price
1599
+ and quote before any Stripe I/O. Conflicting bindings, unavailable authority,
1600
+ and legacy intents without an offer fail closed. An existing binding can
1601
+ replay after offer expiry, but does not itself assert payment or grant access.
1602
+ Never delete an ambiguous checkout intent to obtain a new quote or purchase.
1603
+ Install the chapter projection schema before deploying the updated client;
1604
+ install `createMembershipAuthorityIntegration()` only in the network owner's
1605
+ existing database (BNF), alongside its policy and verified commercial-event
1606
+ integrations. It creates eight deny-all collections with no activation seeds.
1607
+ Pair it with `createMembershipAuthorityStorage(db, allowedSecretNames)` from
1608
+ the Worker entry: this maps the command model's three `network*` collections
1609
+ to `membershipRecord`, `membershipEntitlement`, and `membershipSeat`, leaving
1610
+ the original shadow collections unchanged. Atomic guards and mutation IDs
1611
+ are preserved. Do not mix this descriptor with raw `membershipOfferEntities`
1612
+ or `membershipAuthorityEntities`, or use its storage without the descriptor.
1613
+ `createMembershipAuthorityHandler` may be mounted in the existing BNF backend;
1614
+ no additional app/database is required. It accepts only signed, exact-scope
1615
+ commands, never arbitrary database operations or role grants. The storage
1616
+ adapter is a defensive contract, not a security boundary against a compromised
1617
+ host holding its unrestricted app key. Protected superadmin changes remain
1618
+ exclusively in odla Studio. `memberSystemAvailability` requires explicit
1619
+ `bnfBackendReachable` independently of `bnfFrontendReachable` and odla health;
1620
+ absent backend health fails dependent workflows closed.
1621
+ Network policy/cohort writes must fence `membershipAuthorityState.revision`.
1622
+ Both the chapter and network Stripe destinations must subscribe to
1623
+ `invoice.payment_succeeded` before enabling the adapter. Its membership flow
1624
+ ignores `invoice.paid` because that event also includes out-of-band payments.
1625
+ The network independently verifies the event, scope, provider references and
1626
+ frozen amount before atomically creating a pending membership/entitlement.
1627
+ A chapter webhook arriving first retries without advancing paid status.
1628
+ Durable chapter Journey approval then obtains the network member number and
1629
+ active entitlement before Clerk promotion; duplicate approvals do not spend
1630
+ another number. Already-paid founding promises can exceed the soft target.
1631
+ Full renewal/refund/seat lifecycle wiring and live provider journey proof
1632
+ remain prerequisites for enabling this adapter in a host.
1633
+
1634
+ `drainMembershipEffects` recovers pending provider work and expires unpaid or
1635
+ unaccepted seats within one exact configured runtime. Pair its
1636
+ `deliverChapterEffect` callback with `deliverMembershipChapterEffect` and a
1637
+ chapter `createMembershipChapterEffectRoute({ sender, secretName, origin,
1638
+ readEffect: adapter.readEffect })`. The receiver reloads current network state
1639
+ before inviting, granting or revoking; a stale queue item cannot specify an
1640
+ arbitrary recipient or grant access. Missing adapters and ambiguous provider
1641
+ receipts remain pending. `retryChapterApprovalEffects` separately repairs
1642
+ durable, unfinished local approval work without starving later applicants.
1643
+ Neither helper installs a cron or provisions infrastructure by itself.
1644
+
1645
+ The member area exposes the authenticated purchaser's named-seat checkout
1646
+ only when configured. Recipient hosts pass `initialNamedSeatId` from the
1647
+ invitation's `seat` query parameter and an authenticated `namedSeatClaimApi`
1648
+ to `JoinIsland`; the recipient completes their own application and acceptance
1649
+ without a second subscription checkout. A primary cancellation also governs
1650
+ seat renewal; buying a seat does not restore canceled auto-renewal.
1651
+
1652
+ Delivery logs distinguish sent, logged-only, in-flight, uncertain and
1653
+ retry-approved outcomes. The authenticated operator resolution route
1654
+ `/api/admin/email/log/:id/resolve` records evidence against the exact attempt;
1655
+ it never sends mail. Confirmed non-delivery permits only the original workflow
1656
+ to retry its frozen payload. Non-production recipient redirection still applies.
1508
1657
 
1509
1658
  ### Install + scope notes
1510
1659
 
1511
- - **Bootstrap the first admin according to the selected auth source.** Chapter
1512
- mode defaults to Clerk claims: set the first operator's Clerk
1513
- `public_metadata.role` to `admin`. If that operator also needs the
1514
- super-admin tier, prefer `auth: { superAdminSource: "odla" }` and grant their
1515
- app-scoped role on Database → Users in odla Studio. Existing sites retain the
1516
- legacy `superAdmins` table until they opt in. Hub mode defaults to table auth:
1660
+ - **Bootstrap the first admin according to the selected auth source.** Prefer
1661
+ `auth: { source: "clerk", superAdminSource: "odla" }`: the backend resolves
1662
+ `private_metadata.role` on each authorization check, ignores public/session
1663
+ roles, and fails closed if Clerk is unavailable. Grant the operator's
1664
+ app-scoped protected role on Database → Users in odla Studio, bound to the
1665
+ exact Clerk subject ID (not email). This protected grant is separate from
1666
+ their private Clerk `admin` role and is never imported from legacy metadata.
1667
+ Existing Chapter sites retain claim/public-role defaults until an explicit,
1668
+ coordinated migration; do not clear public roles while a peer runtime still
1669
+ depends on them. Hub mode defaults to table auth:
1517
1670
  add the first operator's lowercase
1518
1671
  email to `admins` in Studio. Neither allowlist is ever written by a worker
1519
1672
  route or provisioning seed; that is deliberate, so the running app cannot
@@ -1541,9 +1694,17 @@ These bite silently — a smoke test won't catch them:
1541
1694
  - **Roster/identity:** `GET /people` (union of `$users` + applications by email,
1542
1695
  roles and backend-only profile signals from `clerkListUsers`),
1543
1696
  `GET /people/access`, `POST /people/role`, `POST /crm/sync`
1544
- (backfill/reproject), and
1697
+ (empty body: backfill/reproject; `{ recordId }`: repair only that person's
1698
+ missing application link). Targeted repair requires a unique application
1699
+ and CRM person match in this chapter, preserves profile and lifecycle state,
1700
+ and records an audited compare-and-swap update. Ambiguity fails closed. The
1701
+ Stage panel offers this repair when an application link is missing. Also,
1545
1702
  `POST /clerk/private-profiles/sync?offset=0&limit=50` (bounded,
1546
1703
  resumable migration/repair from Chapter's former public profile keys).
1704
+ New default chapter CRMs declare `applicationId` and `tier`; initial applicant
1705
+ projection preserves these links even if optional enrichment is invalid.
1706
+ Submission replays use the canonical stored application, and reject changed
1707
+ email before account provisioning, notifications, or CRM projection.
1547
1708
  - **Pipeline/meetings:** `GET /dashboard` (flow counts, stage counts + weekly
1548
1709
  delta, agenda, live revenue), `GET /meetings` (reconciled agenda),
1549
1710
  `GET/PUT /scheduling`, `POST /meetings/:id/{reschedule,cancel}`,
@@ -1564,6 +1725,25 @@ These bite silently — a smoke test won't catch them:
1564
1725
  }});
1565
1726
  ```
1566
1727
 
1728
+ Refund execution is subscription-scoped: the provider verifies the customer and
1729
+ the admin route's exact application/chapter/environment/runtime metadata, then
1730
+ resolves the subscription's first paid invoice. It never picks an arbitrary
1731
+ customer charge or moves to a renewal when the original charge was refunded.
1732
+ Modern invoice-payment records and legacy invoice charge/payment-intent fields
1733
+ are supported; ambiguous payments or incomplete history fail closed. Retries
1734
+ reuse a stable refund key and can finish cancellation even after the refund
1735
+ webhook updates the application. A partial `charge.refunded` event is not a
1736
+ terminal membership or gift event. Customer-only refund events are quarantined
1737
+ for reconciliation rather than guessed from a customer's latest application.
1738
+ Before reporting success, the refund command re-reads the exact charge and
1739
+ verifies a completed full refund, then reconciles the application and CRM.
1740
+ This also repairs older payments whose charge events lack application metadata;
1741
+ a pending refund is not reported as complete. Closed billing has no renewal date.
1742
+ New checkouts resolve legacy or modern InvoicePayments and verify/tag the exact
1743
+ PaymentIntent before exposing its client secret. Missing, foreign, or ambiguous
1744
+ payment evidence fails closed. Booking enforces the same payment and waiver
1745
+ eligibility as the signup wizard, including on direct endpoint requests.
1746
+
1567
1747
  The escalation rules (super-admin tier, self-demotion lockout) are **not** seams
1568
1748
  — `canChangeRole` package-enforces them so a site can't weaken them. `approve`
1569
1749
  is the caller `onboardingInvite` was missing.
@@ -1573,18 +1753,27 @@ These bite silently — a smoke test won't catch them:
1573
1753
  the odla→Clerk write half: `clerkGetUserByEmail`, `clerkGetUser`,
1574
1754
  `clerkListUsers` (auto-paginated — never a silent 100-user cap),
1575
1755
  `clerkSetRole`, `updateClerkUserMetadata`, and
1576
- `updateClerkUserMetadataByEmail`. An absent `public_metadata.role` reads as
1577
- the lowest rung (`"provisional"`); normalized `profile` and `applicationId`
1578
- reads come only from backend-only private metadata; and `clerkSetRole`
1579
- merge-`PATCH`es only the public role, so it cannot clobber the private profile.
1580
- All take an injectable `fetch` and the vault `clerk_secret_key`.
1756
+ `updateClerkUserMetadataByEmail`. Role helpers accept a final metadata source
1757
+ (`"private"` or legacy `"public"`, after the injectable `fetch`); use
1758
+ `clerkRoleMetadata(chapter.auth)` so readers and writers agree. An absent
1759
+ selected role reads as `"provisional"`. Private role writes merge only that
1760
+ key and remove the legacy public role, preserving private profile data.
1761
+ Helpers use the effective vault `clerk_secret_key`; no extra credential is
1762
+ needed. `planPrivateRoleMigration` is a dev-only, dry-run preflight requiring
1763
+ the complete active runtime inventory, paused role writers, compatible builds,
1764
+ and an existing protected operator grant. It rejects conflicting roles and
1765
+ never grants privileges or writes data. Follow the adoption runbook for
1766
+ coordinated migration and same-session revocation tests.
1581
1767
 
1582
1768
  ### Multi-site acceptance tests
1583
1769
 
1584
1770
  `@odla-ai/chapter/testing` exports a schema-validated manifest, scenario planner,
1585
1771
  public join-contract probe, serial suite runner, and text report formatter.
1586
- Declare every logical chapter and every authoritative developer runtime in the
1587
- manifest; runtime identity is separate from Stripe test/live mode.
1772
+ Declare every logical chapter in the reviewed deployment scope and its exact
1773
+ developer runtime. Another developer's authoritative sandbox is not implicitly
1774
+ a deployment prerequisite or a target for writes. Keep sibling-runtime isolation
1775
+ tests without requiring deployment access to those siblings. Runtime identity is
1776
+ separate from Stripe test/live mode.
1588
1777
 
1589
1778
  - `probeChapterJoinContracts(manifest)` reads deployed health and catalog routes.
1590
1779
  It compares effective app/chapter/data-environment/runtime health headers with
@@ -1606,6 +1795,29 @@ manifest; runtime identity is separate from Stripe test/live mode.
1606
1795
  - Evidence is copied only after validation. Extra fields and rejected payloads
1607
1796
  are discarded; check labels must contain no personal data, and references must
1608
1797
  be opaque provider IDs or digests, never client secrets or raw provider objects.
1798
+ - `assessChapterReleaseEvidence(plan, proofs)` validates a separately reviewed
1799
+ required-proof contract against exact source commits and dev Worker versions.
1800
+ Missing features must remain requirements even if current feature flags are
1801
+ off. Missing/duplicate proofs, wrong execution phases or runtimes, failed
1802
+ assertions, absent provider references, and incomplete cleanup block review.
1803
+ This checks evidence completeness, not whether a driver actually called a
1804
+ provider; real driver execution and inspectable artifacts remain mandatory.
1805
+ A complete result is only `readyForReview`; `productionAuthorized` is always
1806
+ false, and this API never deploys anything.
1807
+
1808
+ Host routes can reuse `resolveNetworkTargetTransport` from the Worker entry
1809
+ for public discovery as well as signed delivery. A declared service binding is
1810
+ authoritative: missing or invalid bindings throw `NetworkTargetTransportError`
1811
+ instead of falling back to a public URL. Hosts remain responsible for selecting
1812
+ the reviewed runtime/origin and bounding response size and duration.
1813
+
1814
+ `ChapterAdmin.renderWorkspaceFrame` replaces packaged workspace geometry with
1815
+ a host-owned frame. It receives the already-composed active content, workspace
1816
+ list (including optional icons), current account and native route/navigation
1817
+ helpers. Custom headers remain outside the frame; auth and active-only mounting
1818
+ stay in Chapter. Hosts need no per-workspace renderer wrappers or duplicate
1819
+ navigation registry. Without the slot, existing tab and standalone layouts stay
1820
+ unchanged.
1609
1821
 
1610
1822
  ### Verify from the types, not this file
1611
1823