@oimlsmart/platform-server 0.1.7 → 0.1.9

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/src/store.ts CHANGED
@@ -590,7 +590,10 @@ export interface OpClientRoleAssignment {
590
590
  * account's pending ceremony state). TODO.identity-features/08 adds
591
591
  * `personalAccessTokens`: the developer-token rows (a dead account's
592
592
  * tokens die with it). TODO.identity-features/12 adds `consentGrants`:
593
- * the remembered consent rows (a dead account's grants die with it). */
593
+ * the remembered consent rows (a dead account's grants die with it).
594
+ * TODO.identity-features/01 adds `emails`: the additional-address rows
595
+ * (every address of the account goes — the tombstone's primary is the
596
+ * anonymized users.email). */
594
597
  export interface OpAccountErasure {
595
598
  sessions: number
596
599
  accessTokens: number
@@ -603,21 +606,28 @@ export interface OpAccountErasure {
603
606
  factors: number
604
607
  personalAccessTokens: number
605
608
  consentGrants: number
609
+ emails: number
606
610
  }
607
611
 
608
612
  // ── the account console (TODO.identity/06) ───────────────────────────
609
613
 
610
- /** The verify-new-email ceremony's row (the enrollment link's doctrine:
614
+ /** The verify-an-address ceremony's row (the enrollment link's doctrine:
611
615
  * a 256-bit random token backed by the D1 row; one-time, 24 h).
612
616
  * deliveredBy records the channel the link traveled: 'mailer' (sent to
613
617
  * the NEW address; completing verifies it) or 'shown' (no mailer
614
618
  * configured, the link was displayed to the signed-in holder; the change
615
- * applies but the address stays unverified, honestly). */
619
+ * applies but the address stays unverified, honestly).
620
+ * TODO.identity-features/01: kind names the ceremony — 'change' (the
621
+ * primary-address replacement; completion moves users.email) or 'add'
622
+ * (the per-address verification of an account_emails row; completion
623
+ * stamps the row's verified_at). Rows predating the kind column read
624
+ * 'change' (the migration's default). */
616
625
  export interface EmailChangeToken {
617
626
  token: string
618
627
  userId: string
619
628
  newEmail: string
620
629
  deliveredBy: 'mailer' | 'shown'
630
+ kind: 'change' | 'add'
621
631
  createdAt: string
622
632
  expiresAt: string
623
633
  consumedAt: string | null
@@ -635,6 +645,38 @@ export type CompleteEmailChangeResult =
635
645
  * (the token is burned; the change must start over). */
636
646
  | { kind: 'conflict' }
637
647
 
648
+ // ── multiple emails per account (TODO.identity-features/01) ──────────
649
+
650
+ /** One of the account's addresses. The PRIMARY is the users row's email
651
+ * (isPrimary — the OIDC `email` claim's source, never an
652
+ * account_emails row); the ADDITIONAL addresses are the account_emails
653
+ * rows. verifiedAt NULL = the mailbox is unproven: an unverified
654
+ * additional NEVER names the account (not to sign-in, not to recovery,
655
+ * never a notification's target). */
656
+ export interface AccountEmail {
657
+ userId: string
658
+ email: string
659
+ verifiedAt: string | null
660
+ isPrimary: boolean
661
+ /** Who added the row (the holder's session email, an admin's); NULL on
662
+ * the primary line (the users row carries no such provenance). */
663
+ addedBy: string | null
664
+ createdAt: string
665
+ }
666
+
667
+ /** The additional-address add's honest outcomes. */
668
+ export type AddAccountEmailResult =
669
+ /** The row landed (unverified — the verification ceremony follows). */
670
+ | 'added'
671
+ /** The account ALREADY carries the address as an additional (the add
672
+ * is an idempotent no-op; the route re-sends the verification for an
673
+ * unverified row). */
674
+ | 'present'
675
+ /** Another account holds the address (as its primary or an
676
+ * additional), or it IS this account's primary — an address names at
677
+ * most one account across the estate. */
678
+ | 'conflict'
679
+
638
680
  // ── strong authentication: the factor registry (TODO.identity-sso/02 + /03)
639
681
 
640
682
  /** A registered passkey (the webauthn_credentials row). publicKeyCose is
@@ -1581,7 +1623,13 @@ export interface ServerStore {
1581
1623
  /** The password sign-in's lookup: the credential + the account's
1582
1624
  * active flag by email (normalized). Null = no such credential — the
1583
1625
  * route still runs one full-cost verify (the timing-shape rule,
1584
- * auth/passwords.ts). The hash never leaves the server. */
1626
+ * auth/passwords.ts). The hash never leaves the server.
1627
+ * TODO.identity-features/01: the address resolves by ANY of the
1628
+ * account's VERIFIED addresses — the primary (users.email) or a
1629
+ * proven account_emails row. An unverified additional never resolves
1630
+ * (the mailbox is unproven), and a primary owner always wins over an
1631
+ * additional row (the deterministic rule — a stray duplicate shadows,
1632
+ * never ambiguates). */
1585
1633
  getPasswordLogin(email: string): Promise<{ userId: string; hash: string; active: boolean } | null>
1586
1634
  /** Set/replace the account's password credential (enrollment
1587
1635
  * completion, the account page's change). */
@@ -1680,15 +1728,20 @@ export interface ServerStore {
1680
1728
  * "sign out everywhere else" action + the password change's
1681
1729
  * best-practice revocation). Answers the revoked count. */
1682
1730
  deleteOtherSessions(userId: string, keepToken: string): Promise<number>
1683
- /** Mint the verify-new-email ceremony's token. A fresh request VOIDS
1684
- * the account's earlier pending rows (only the newest link works).
1685
- * deliveredBy is stamped at request time and decides whether
1686
- * completion may verify the address. */
1731
+ /** Mint the verify-an-address ceremony's token. The void rule keeps
1732
+ * one live link per ceremony target: a 'change' request VOIDS the
1733
+ * account's earlier pending 'change' rows (only the newest change
1734
+ * link works the pre-01 doctrine); an 'add' request voids the
1735
+ * account's earlier pending 'add' rows FOR THE SAME address (other
1736
+ * addresses' links stand). deliveredBy is stamped at request time and
1737
+ * decides whether completion may verify the address. kind defaults
1738
+ * 'change'. */
1687
1739
  createEmailChangeToken(input: {
1688
1740
  token: string
1689
1741
  userId: string
1690
1742
  newEmail: string
1691
1743
  deliveredBy: 'mailer' | 'shown'
1744
+ kind?: 'change' | 'add'
1692
1745
  ttlMs: number
1693
1746
  }): Promise<EmailChangeToken>
1694
1747
  getEmailChangeToken(token: string): Promise<EmailChangeToken | null>
@@ -1696,12 +1749,51 @@ export interface ServerStore {
1696
1749
  * so the console can show it. */
1697
1750
  getPendingEmailChange(userId: string): Promise<EmailChangeToken | null>
1698
1751
  /** Complete the ceremony: consume the token ATOMICALLY (a presented
1699
- * link works exactly once, expired or not), judge the expiry, re-check
1700
- * the address's uniqueness (a conflict burns the token honestly), then
1701
- * move the account's email. verified = the token traveled by mailer
1702
- * (mailbox proven); a shown link never verifies. */
1752
+ * link works exactly once, expired or not), judge the expiry, then
1753
+ * act on the kind: 'change' re-checks the address's uniqueness across
1754
+ * BOTH address tables (a conflict burns the token honestly) and moves
1755
+ * the account's primary (users.email); 'add' stamps the
1756
+ * account_emails row's verified_at (a row removed between request and
1757
+ * completion answers 'unknown'). verified = the token traveled by
1758
+ * mailer (mailbox proven); a shown link never verifies. */
1703
1759
  completeEmailChange(token: string): Promise<CompleteEmailChangeResult>
1704
1760
 
1761
+ // ── multiple emails per account (TODO.identity-features/01) ──
1762
+ /** The account's addresses, the PRIMARY first (the users row's email +
1763
+ * its verification stamp), then the additional account_emails rows
1764
+ * (oldest first). The console's emails section and the security-mail
1765
+ * fan-out read this. */
1766
+ listAccountEmails(userId: string): Promise<AccountEmail[]>
1767
+ /** Resolve the account by ANY of its addresses (normalized): the
1768
+ * primary always names it; an additional ONLY when verified (an
1769
+ * unproven address never names the account — not to sign-in, not to
1770
+ * recovery). The primary owner wins over an additional row (the
1771
+ * deterministic rule). */
1772
+ findUserByAnyEmail(email: string): Promise<AuthUserPayload | null>
1773
+ /** Add an ADDITIONAL address (normalized lowercase; the row lands
1774
+ * UNVERIFIED — the verify-an-address ceremony's kind 'add' token
1775
+ * proves the mailbox). The tagged result names the outcome; the
1776
+ * unique index + the cross-table check make an address name at most
1777
+ * one account. */
1778
+ addAccountEmail(userId: string, email: string, addedBy?: string | null): Promise<AddAccountEmailResult>
1779
+ /** The verification ceremony's stamp on the account's OWN additional
1780
+ * row (the kind 'add' completion): verified_at flips, once (the
1781
+ * guarded update). Answers false when no such row stands. */
1782
+ markAccountEmailVerified(userId: string, email: string): Promise<boolean>
1783
+ /** Promote a VERIFIED additional to primary: the promoted address
1784
+ * becomes users.email (its verification stamp travels, so the claims
1785
+ * stay verified), and the previous primary takes the row's place in
1786
+ * account_emails with ITS stamp (it stays a verified additional —
1787
+ * sign-in by it keeps working). 'unknown' = no such additional row;
1788
+ * 'unverified' = the row stands unproven (a primary is always
1789
+ * proven). */
1790
+ setPrimaryAccountEmail(userId: string, email: string): Promise<'ok' | 'unknown' | 'unverified'>
1791
+ /** Remove an ADDITIONAL address. 'primary' = the address IS the
1792
+ * account's primary (promote another first — the primary is never
1793
+ * removed from under the holder); 'unknown' = no such additional
1794
+ * row. */
1795
+ removeAccountEmail(userId: string, email: string): Promise<'ok' | 'primary' | 'unknown'>
1796
+
1705
1797
  // ── strong authentication: the factor registry (TODO.identity-sso/02 + /03) ──
1706
1798
  /** The one-time WebAuthn ceremony challenge. The challenge value IS the
1707
1799
  * key (the clientDataJSON binds it); expires_at = now + ttlMs. */
@@ -1873,6 +1965,11 @@ export interface ServerStore {
1873
1965
  listOrgMemberships(userId: string): Promise<OrgMembership[]>
1874
1966
  /** One org's memberships (the per-org view), every state. */
1875
1967
  listOrgMembers(orgId: string): Promise<OrgMembership[]>
1968
+ /** EVERY membership across organizations, every state (the admin
1969
+ * registry's org list groups it in memory — one read, never a
1970
+ * per-org loop of listOrgMembers). Org- then creation-ordered, so a
1971
+ * caller's group-by-org keeps listOrgMembers' per-org ordering. */
1972
+ listAllOrgMemberships(): Promise<OrgMembership[]>
1876
1973
  getOrgMembership(userId: string, orgId: string): Promise<OrgMembership | null>
1877
1974
  /** Create the membership — the org's admin inviting an EXISTING
1878
1975
  * account (state 'invited': the holder accepts from the account