@ai-matrx/auth 0.0.0 → 0.1.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/LICENSE +21 -0
  3. package/README.md +31 -1
  4. package/dist/auth-cookie-DWx6Q3xo.d.cts +168 -0
  5. package/dist/auth-cookie-DWx6Q3xo.d.ts +168 -0
  6. package/dist/cookies.cjs +233 -0
  7. package/dist/cookies.d.cts +109 -0
  8. package/dist/cookies.d.ts +109 -0
  9. package/dist/cookies.js +210 -0
  10. package/dist/credentials.cjs +68 -0
  11. package/dist/credentials.d.cts +82 -0
  12. package/dist/credentials.d.ts +82 -0
  13. package/dist/credentials.js +47 -0
  14. package/dist/index.cjs +189 -0
  15. package/dist/index.d.cts +86 -0
  16. package/dist/index.d.ts +86 -0
  17. package/dist/index.js +166 -0
  18. package/dist/next.cjs +655 -0
  19. package/dist/next.d.cts +389 -0
  20. package/dist/next.d.ts +389 -0
  21. package/dist/next.js +637 -0
  22. package/dist/node.cjs +61 -0
  23. package/dist/node.d.cts +33 -0
  24. package/dist/node.d.ts +33 -0
  25. package/dist/node.js +36 -0
  26. package/dist/organizations.cjs +389 -0
  27. package/dist/organizations.d.cts +226 -0
  28. package/dist/organizations.d.ts +226 -0
  29. package/dist/organizations.js +366 -0
  30. package/dist/react.cjs +206 -0
  31. package/dist/react.d.cts +163 -0
  32. package/dist/react.d.ts +163 -0
  33. package/dist/react.js +186 -0
  34. package/dist/session-DhRO4euL.d.cts +102 -0
  35. package/dist/session-DhRO4euL.d.ts +102 -0
  36. package/dist/session-identity.cjs +125 -0
  37. package/dist/session-identity.d.cts +100 -0
  38. package/dist/session-identity.d.ts +100 -0
  39. package/dist/session-identity.js +104 -0
  40. package/dist/testing.cjs +67 -0
  41. package/dist/testing.d.cts +38 -0
  42. package/dist/testing.d.ts +38 -0
  43. package/dist/testing.js +46 -0
  44. package/package.json +186 -6
package/CHANGELOG.md ADDED
@@ -0,0 +1,32 @@
1
+ # Changelog — @ai-matrx/auth
2
+
3
+ ## 0.1.1
4
+
5
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
6
+ `git diff npm/auth/v0.1.0..npm/auth/v0.1.1 -- apps/shared/auth`).
7
+ No source changes intended and no consumer action required.
8
+
9
+ ## 0.1.0 — 2026-10-09
10
+
11
+ First release. Approved by Arman 2026-10-09 ("auth: sign-in, session, organizations").
12
+
13
+ - Root: sign-in/out doors (`signInWithPassword`, `signInWithOAuth`, `signOut`, `getSession`,
14
+ `getPerson`, `onSessionChange`, `updatePerson`, new `refreshSession`), `sessionCredentials`
15
+ (the one session → `user` credential mapping; `userCredentials` kept as its shipped name),
16
+ session storage per platform (`memoryStorage`, `browserStorage`, `extensionStorage`,
17
+ `keyValueStorage`; `/node` `fileStorage`), and `createAuthClient` (refuses a secret key).
18
+ - `/organizations`: the active-organization ladder, membership/account-choice/favourite doors and
19
+ the refusal classifier, plus NEW `createActiveOrganization` — the held-request store
20
+ (`ensureOrgId` waits for the ladder, refuses by name with none, a record's org wins; `switchTo`
21
+ saves last active; identity-keyed per-tab hold; sign-out resets). Never a list filter.
22
+ - `/react`: `useSession`, `useActiveOrganization`, `useSessionIdentity`.
23
+ - MOVED IN, behavior verbatim: `@ai-matrx/data/auth` (root), `/organizations`,
24
+ `/session-identity`, `/testing`, `/next` (every Next.js client), the auth and active-org cookies
25
+ (`/cookies`, from `@ai-matrx/data/db`); the five-kind credentials port (`/credentials`, from
26
+ `@ai-matrx/api/credentials`).
27
+
28
+ ### Consumer action
29
+
30
+ None. Every moved name keeps its old path (`@ai-matrx/data/*`, `@ai-matrx/api/credentials`
31
+ re-export from here). New code imports `@ai-matrx/auth`.
32
+
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AI Matrix Engine
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,33 @@
1
1
  # @ai-matrx/auth
2
2
 
3
- Reserved for the planned AI Matrx shared authentication package. This `0.0.0` release reserves the canonical package name; it does not contain an implementation yet.
3
+ Sign-in, session and organizations for every AI Matrx app — the small package at the top of the
4
+ app. Framework-free core; `/react` and `/next` sections; pay only for the sections you import.
5
+
6
+ ```ts
7
+ import { createAuthClient, signInWithPassword, sessionCredentials, signOut } from "@ai-matrx/auth";
8
+ import { createActiveOrganization, readMemberships } from "@ai-matrx/auth/organizations";
9
+
10
+ const supabase = createAuthClient({ url: "https://db.matrxserver.com", key: PUBLISHABLE_KEY });
11
+ const { person } = await signInWithPassword(supabase, { email, password });
12
+ const active = createActiveOrganization({ client: supabase });
13
+ const orgId = await active.ensureOrgId(); // held until the ladder answers
14
+ const credentials = sessionCredentials(supabase); // hand to @ai-matrx/api's createApi
15
+ ```
16
+
17
+ | Section | What |
18
+ |---|---|
19
+ | `@ai-matrx/auth` | sign-in/out, session, refresh, `sessionCredentials`, storage per platform, `createAuthClient` |
20
+ | `/organizations` | memberships, favourites, the active-organization ladder and `createActiveOrganization` |
21
+ | `/credentials` | the five credential kinds and the one credential → header mapping |
22
+ | `/session-identity` | who is acting, live from the session |
23
+ | `/cookies` | the domain-wide auth cookie and the active-organization cookie |
24
+ | `/next` | every Next.js Supabase client (browser, RSC, route, middleware) |
25
+ | `/react` | `useSession`, `useActiveOrganization`, `useSessionIdentity` |
26
+ | `/node` | `fileStorage` — a script's session survives between runs |
27
+ | `/testing` | fake sessions for tests |
28
+
29
+ Session storage: browser `browserStorage()`, extension `extensionStorage()`, desktop
30
+ `keyValueStorage(store)`, Node `fileStorage(path)` or `memoryStorage()`, Next.js `/next`.
31
+
32
+ The active organization is never a list filter: reads show everything the person can see; the
33
+ active organization is only where new things are saved and which organization a server call runs in.
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Shared auth-cookie options for EVERY Supabase client an application
3
+ * constructs (browser, server, middleware, OAuth callbacks). All construction
4
+ * sites MUST pass these options — a client using the default cookie name
5
+ * cannot see the session the others wrote.
6
+ *
7
+ * WHY (the 2026-07 Matrx deployment split): one login must work on every
8
+ * subdomain of the platform's apex (e.g. app.example.com, manage.example.com),
9
+ * so on those hosts the auth cookie is issued domain-wide
10
+ * (`Domain=.example.com`) instead of Supabase's default host-only cookie.
11
+ *
12
+ * The cookie is also RENAMED (default `sb-<project-ref>-auth-token` →
13
+ * `sb-matrx-auth`): re-issuing an existing host-only cookie as domain-wide
14
+ * under the SAME name leaves two same-name cookies in the browser (different
15
+ * Domain attributes = distinct cookies), and whichever the browser sends
16
+ * first wins — auth flaps. A new name makes the transition deterministic:
17
+ * every user re-logs-in exactly once and the old cookie expires unused.
18
+ * The `sb-` prefix is kept on purpose — cleanup paths that scrub session
19
+ * cookies delete by that prefix.
20
+ *
21
+ * The Domain attribute is ONLY set on real apex hosts. localhost and preview
22
+ * hosts get a host-only cookie — a browser rejects any Set-Cookie whose
23
+ * Domain does not cover the current host, so setting it unconditionally would
24
+ * silently break auth everywhere else.
25
+ *
26
+ * The apex domain is a VALUE the host passes in (never read from an env var
27
+ * here): `createAuthCookie({ apexDomain: "aimatrx.com" })`.
28
+ *
29
+ * THE RENAME MIGRATION (0.5.0 — moved in from matrx-frontend, where it was
30
+ * 44 lines of host code every other consumer would have had to re-derive):
31
+ * replacing the auth authority (a new Supabase project) means a NEW cookie
32
+ * name, and a tab still running the OLD bundle keeps writing the OLD name for
33
+ * days. `migrateLegacyCookies` seeds the new storage key from the old one
34
+ * ONLY when the new key is absent — never overwriting a live session — and
35
+ * `isCurrentCookie` recognizes both the base name and Supabase's numbered
36
+ * chunk suffixes (`<name>.0`, `<name>.1`, …), which a naive `startsWith`
37
+ * check gets wrong for any cookie that merely shares the prefix.
38
+ */
39
+ /** Structurally compatible with @supabase/ssr's CookieOptionsWithName. */
40
+ interface AuthCookieOptions {
41
+ name: string;
42
+ /** Remote hosts require HTTPS; loopback development remains usable over HTTP. */
43
+ secure: boolean;
44
+ domain?: string;
45
+ }
46
+ /** A cookie as read from a request — name + value, nothing else needed. */
47
+ interface AuthCookieValue {
48
+ name: string;
49
+ value: string;
50
+ }
51
+ declare const DEFAULT_AUTH_COOKIE_NAME = "sb-matrx-auth";
52
+ interface CreateAuthCookieArgs {
53
+ /** The apex domain one login must span (e.g. `"aimatrx.com"`). */
54
+ apexDomain: string;
55
+ /** Override only for a second, unrelated deployment — never per-app. */
56
+ cookieName?: string;
57
+ /**
58
+ * The storage key a PREVIOUS auth authority used. When present, cookies
59
+ * under this name are seeded onto `cookieName` (absent-only) by
60
+ * `migrateLegacyCookies`, and the middleware binding clears them at the
61
+ * scope this host issued them on.
62
+ */
63
+ legacyCookieName?: string;
64
+ }
65
+ interface AuthCookie {
66
+ cookieName: string;
67
+ /** The superseded storage key, when this deployment has one. */
68
+ legacyCookieName: string | undefined;
69
+ /**
70
+ * Cookie options for the host serving the current request.
71
+ * @param host `Host` header or `window.location.hostname` (port tolerated).
72
+ */
73
+ optionsForHost(host: string | null | undefined): AuthCookieOptions;
74
+ /** Browser variant — resolves the host from `window` (SSR-safe). */
75
+ browserOptions(): AuthCookieOptions;
76
+ /**
77
+ * Is this cookie name the CURRENT auth cookie — base name or one of
78
+ * Supabase's numbered chunks (`<name>.0`)? A prefix test is wrong: it
79
+ * matches `<name>.anything`, including unrelated cookies.
80
+ */
81
+ isCurrentCookie(name: string): boolean;
82
+ /**
83
+ * Cookies to seed under the current name, derived from the legacy name.
84
+ * Returns `[]` when any current cookie already exists (never overwrite a
85
+ * live session) or when no legacy name is configured.
86
+ */
87
+ migrateLegacyCookies(cookies: readonly AuthCookieValue[]): AuthCookieValue[];
88
+ /**
89
+ * THE POISONED-JAR READ. A browser can hold the SAME auth cookie name twice
90
+ * — once host-only, once at `.apex` — because different Domain attributes
91
+ * make distinct cookies, and it sends both in one `Cookie` header. Every
92
+ * reassembler downstream (`@supabase/ssr`'s `combineChunks`) resolves a
93
+ * chunk with `find()`, i.e. FIRST WINS, and the browser decides the order.
94
+ * An empty shadow that sorts first therefore reassembles to `""` and every
95
+ * server read answers anonymous while the tab is signed in.
96
+ *
97
+ * This collapses each auth-cookie NAME to one entry, preferring the first
98
+ * NON-EMPTY value. Non-auth cookies pass through untouched, in order.
99
+ *
100
+ * A `Cookie` header carries no Domain, so which scope a value came from is
101
+ * genuinely unknowable here — non-empty-wins is the strongest correct rule
102
+ * available, and `@supabase/ssr` already refuses a chunk set that decodes
103
+ * incoherently. `shadowedNames` names every auth cookie that arrived more
104
+ * than once, so the caller can EXPIRE the losing scope instead of leaving
105
+ * the user to clear cookies by hand.
106
+ *
107
+ * THE SECOND POISON, AND IT IS AN IDENTITY LEAK: one storage key arriving
108
+ * BOTH unchunked (`<name>`) and chunked (`<name>.0`, `<name>.1`). These are
109
+ * DIFFERENT cookie names, so the per-name collapse above cannot see them,
110
+ * and `@supabase/ssr`'s `combineChunks` returns the UNCHUNKED value the
111
+ * moment it is non-empty — it never even reads the chunks. A stale base
112
+ * cookie therefore shadows a freshly written chunked pair, and every read
113
+ * resolves the PREVIOUS person while the new session sits unread beside it.
114
+ * That is a different user's session being served, not merely a logout.
115
+ *
116
+ * `applyServerStorage` never writes that shape — it removes every chunk-like
117
+ * sibling of whatever it writes — so a jar holding both is RESIDUE from a
118
+ * removal that did not land (see `middlewareSession`: a two-scope removal
119
+ * collapses on `ResponseCookies`, which is how this residue is made).
120
+ *
121
+ * WHICH COPY IS NEWER IS UNKNOWABLE HERE, and both directions of residue are
122
+ * producible, so preferring either shape is a coin flip on WHOSE SESSION TO
123
+ * SERVE. This drops the whole family from the read instead: nobody is signed
124
+ * in, every name in it is returned in `ambiguousNames` to be expired, and the
125
+ * host's honest-screen banner says so with the one action that fixes it. A
126
+ * wrong sign-out is recoverable in one click; a wrong identity is not.
127
+ */
128
+ reconcile(cookies: readonly AuthCookieValue[]): ReconciledAuthCookies;
129
+ }
130
+ /** The outcome of {@link AuthCookie.reconcile}. */
131
+ interface ReconciledAuthCookies {
132
+ /** One entry per name; auth names resolved to their non-empty value. */
133
+ cookies: AuthCookieValue[];
134
+ /** Auth cookie names that arrived more than once (i.e. at two scopes). */
135
+ shadowedNames: string[];
136
+ /**
137
+ * Auth cookie names belonging to a storage key that arrived UNCHUNKED AND
138
+ * CHUNKED at once — see {@link AuthCookie.reconcile}. Their values have been
139
+ * dropped from `cookies`, and they must be expired on EVERY host, including
140
+ * the ones that issue a host-only cookie: unlike a two-scope shadow there is
141
+ * no copy here that could be the good one.
142
+ */
143
+ ambiguousNames: string[];
144
+ /** The storage keys that arrived in both shapes. Named for the scream. */
145
+ ambiguousKeys: string[];
146
+ /** `true` when the jar is split and needs healing. */
147
+ split: boolean;
148
+ }
149
+ /**
150
+ * Parse a raw `Cookie` request header, **PRESERVING DUPLICATES AND ORDER**.
151
+ *
152
+ * 🚨 THIS IS THE ONLY WAY TO SEE A SPLIT JAR. Next's `RequestCookies` (and the
153
+ * `cookies()` store built on it) parses the header into a Map keyed by NAME,
154
+ * so two same-name cookies at two Domain scopes collapse to ONE before any
155
+ * application code runs — and the survivor is whichever the browser happened
156
+ * to send LAST. Measured against the real class:
157
+ *
158
+ * "sb-…-v2.0=; sb-…-v2.0=base64-REAL" -> [{ value: "base64-REAL" }] works
159
+ * "sb-…-v2.0=base64-REAL; sb-…-v2.0=" -> [{ value: "" }] anonymous
160
+ *
161
+ * That coin flip IS the production break, and it is invisible from the
162
+ * collapsed jar: reconciliation over `getAll()` is a no-op no matter how
163
+ * carefully it is written. Every door must therefore reconcile over THIS.
164
+ */
165
+ declare function parseCookieHeader(header: string | null | undefined): AuthCookieValue[];
166
+ declare function createAuthCookie(args: CreateAuthCookieArgs): AuthCookie;
167
+
168
+ export { type AuthCookie as A, type CreateAuthCookieArgs as C, DEFAULT_AUTH_COOKIE_NAME as D, type ReconciledAuthCookies as R, type AuthCookieOptions as a, type AuthCookieValue as b, createAuthCookie as c, parseCookieHeader as p };
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Shared auth-cookie options for EVERY Supabase client an application
3
+ * constructs (browser, server, middleware, OAuth callbacks). All construction
4
+ * sites MUST pass these options — a client using the default cookie name
5
+ * cannot see the session the others wrote.
6
+ *
7
+ * WHY (the 2026-07 Matrx deployment split): one login must work on every
8
+ * subdomain of the platform's apex (e.g. app.example.com, manage.example.com),
9
+ * so on those hosts the auth cookie is issued domain-wide
10
+ * (`Domain=.example.com`) instead of Supabase's default host-only cookie.
11
+ *
12
+ * The cookie is also RENAMED (default `sb-<project-ref>-auth-token` →
13
+ * `sb-matrx-auth`): re-issuing an existing host-only cookie as domain-wide
14
+ * under the SAME name leaves two same-name cookies in the browser (different
15
+ * Domain attributes = distinct cookies), and whichever the browser sends
16
+ * first wins — auth flaps. A new name makes the transition deterministic:
17
+ * every user re-logs-in exactly once and the old cookie expires unused.
18
+ * The `sb-` prefix is kept on purpose — cleanup paths that scrub session
19
+ * cookies delete by that prefix.
20
+ *
21
+ * The Domain attribute is ONLY set on real apex hosts. localhost and preview
22
+ * hosts get a host-only cookie — a browser rejects any Set-Cookie whose
23
+ * Domain does not cover the current host, so setting it unconditionally would
24
+ * silently break auth everywhere else.
25
+ *
26
+ * The apex domain is a VALUE the host passes in (never read from an env var
27
+ * here): `createAuthCookie({ apexDomain: "aimatrx.com" })`.
28
+ *
29
+ * THE RENAME MIGRATION (0.5.0 — moved in from matrx-frontend, where it was
30
+ * 44 lines of host code every other consumer would have had to re-derive):
31
+ * replacing the auth authority (a new Supabase project) means a NEW cookie
32
+ * name, and a tab still running the OLD bundle keeps writing the OLD name for
33
+ * days. `migrateLegacyCookies` seeds the new storage key from the old one
34
+ * ONLY when the new key is absent — never overwriting a live session — and
35
+ * `isCurrentCookie` recognizes both the base name and Supabase's numbered
36
+ * chunk suffixes (`<name>.0`, `<name>.1`, …), which a naive `startsWith`
37
+ * check gets wrong for any cookie that merely shares the prefix.
38
+ */
39
+ /** Structurally compatible with @supabase/ssr's CookieOptionsWithName. */
40
+ interface AuthCookieOptions {
41
+ name: string;
42
+ /** Remote hosts require HTTPS; loopback development remains usable over HTTP. */
43
+ secure: boolean;
44
+ domain?: string;
45
+ }
46
+ /** A cookie as read from a request — name + value, nothing else needed. */
47
+ interface AuthCookieValue {
48
+ name: string;
49
+ value: string;
50
+ }
51
+ declare const DEFAULT_AUTH_COOKIE_NAME = "sb-matrx-auth";
52
+ interface CreateAuthCookieArgs {
53
+ /** The apex domain one login must span (e.g. `"aimatrx.com"`). */
54
+ apexDomain: string;
55
+ /** Override only for a second, unrelated deployment — never per-app. */
56
+ cookieName?: string;
57
+ /**
58
+ * The storage key a PREVIOUS auth authority used. When present, cookies
59
+ * under this name are seeded onto `cookieName` (absent-only) by
60
+ * `migrateLegacyCookies`, and the middleware binding clears them at the
61
+ * scope this host issued them on.
62
+ */
63
+ legacyCookieName?: string;
64
+ }
65
+ interface AuthCookie {
66
+ cookieName: string;
67
+ /** The superseded storage key, when this deployment has one. */
68
+ legacyCookieName: string | undefined;
69
+ /**
70
+ * Cookie options for the host serving the current request.
71
+ * @param host `Host` header or `window.location.hostname` (port tolerated).
72
+ */
73
+ optionsForHost(host: string | null | undefined): AuthCookieOptions;
74
+ /** Browser variant — resolves the host from `window` (SSR-safe). */
75
+ browserOptions(): AuthCookieOptions;
76
+ /**
77
+ * Is this cookie name the CURRENT auth cookie — base name or one of
78
+ * Supabase's numbered chunks (`<name>.0`)? A prefix test is wrong: it
79
+ * matches `<name>.anything`, including unrelated cookies.
80
+ */
81
+ isCurrentCookie(name: string): boolean;
82
+ /**
83
+ * Cookies to seed under the current name, derived from the legacy name.
84
+ * Returns `[]` when any current cookie already exists (never overwrite a
85
+ * live session) or when no legacy name is configured.
86
+ */
87
+ migrateLegacyCookies(cookies: readonly AuthCookieValue[]): AuthCookieValue[];
88
+ /**
89
+ * THE POISONED-JAR READ. A browser can hold the SAME auth cookie name twice
90
+ * — once host-only, once at `.apex` — because different Domain attributes
91
+ * make distinct cookies, and it sends both in one `Cookie` header. Every
92
+ * reassembler downstream (`@supabase/ssr`'s `combineChunks`) resolves a
93
+ * chunk with `find()`, i.e. FIRST WINS, and the browser decides the order.
94
+ * An empty shadow that sorts first therefore reassembles to `""` and every
95
+ * server read answers anonymous while the tab is signed in.
96
+ *
97
+ * This collapses each auth-cookie NAME to one entry, preferring the first
98
+ * NON-EMPTY value. Non-auth cookies pass through untouched, in order.
99
+ *
100
+ * A `Cookie` header carries no Domain, so which scope a value came from is
101
+ * genuinely unknowable here — non-empty-wins is the strongest correct rule
102
+ * available, and `@supabase/ssr` already refuses a chunk set that decodes
103
+ * incoherently. `shadowedNames` names every auth cookie that arrived more
104
+ * than once, so the caller can EXPIRE the losing scope instead of leaving
105
+ * the user to clear cookies by hand.
106
+ *
107
+ * THE SECOND POISON, AND IT IS AN IDENTITY LEAK: one storage key arriving
108
+ * BOTH unchunked (`<name>`) and chunked (`<name>.0`, `<name>.1`). These are
109
+ * DIFFERENT cookie names, so the per-name collapse above cannot see them,
110
+ * and `@supabase/ssr`'s `combineChunks` returns the UNCHUNKED value the
111
+ * moment it is non-empty — it never even reads the chunks. A stale base
112
+ * cookie therefore shadows a freshly written chunked pair, and every read
113
+ * resolves the PREVIOUS person while the new session sits unread beside it.
114
+ * That is a different user's session being served, not merely a logout.
115
+ *
116
+ * `applyServerStorage` never writes that shape — it removes every chunk-like
117
+ * sibling of whatever it writes — so a jar holding both is RESIDUE from a
118
+ * removal that did not land (see `middlewareSession`: a two-scope removal
119
+ * collapses on `ResponseCookies`, which is how this residue is made).
120
+ *
121
+ * WHICH COPY IS NEWER IS UNKNOWABLE HERE, and both directions of residue are
122
+ * producible, so preferring either shape is a coin flip on WHOSE SESSION TO
123
+ * SERVE. This drops the whole family from the read instead: nobody is signed
124
+ * in, every name in it is returned in `ambiguousNames` to be expired, and the
125
+ * host's honest-screen banner says so with the one action that fixes it. A
126
+ * wrong sign-out is recoverable in one click; a wrong identity is not.
127
+ */
128
+ reconcile(cookies: readonly AuthCookieValue[]): ReconciledAuthCookies;
129
+ }
130
+ /** The outcome of {@link AuthCookie.reconcile}. */
131
+ interface ReconciledAuthCookies {
132
+ /** One entry per name; auth names resolved to their non-empty value. */
133
+ cookies: AuthCookieValue[];
134
+ /** Auth cookie names that arrived more than once (i.e. at two scopes). */
135
+ shadowedNames: string[];
136
+ /**
137
+ * Auth cookie names belonging to a storage key that arrived UNCHUNKED AND
138
+ * CHUNKED at once — see {@link AuthCookie.reconcile}. Their values have been
139
+ * dropped from `cookies`, and they must be expired on EVERY host, including
140
+ * the ones that issue a host-only cookie: unlike a two-scope shadow there is
141
+ * no copy here that could be the good one.
142
+ */
143
+ ambiguousNames: string[];
144
+ /** The storage keys that arrived in both shapes. Named for the scream. */
145
+ ambiguousKeys: string[];
146
+ /** `true` when the jar is split and needs healing. */
147
+ split: boolean;
148
+ }
149
+ /**
150
+ * Parse a raw `Cookie` request header, **PRESERVING DUPLICATES AND ORDER**.
151
+ *
152
+ * 🚨 THIS IS THE ONLY WAY TO SEE A SPLIT JAR. Next's `RequestCookies` (and the
153
+ * `cookies()` store built on it) parses the header into a Map keyed by NAME,
154
+ * so two same-name cookies at two Domain scopes collapse to ONE before any
155
+ * application code runs — and the survivor is whichever the browser happened
156
+ * to send LAST. Measured against the real class:
157
+ *
158
+ * "sb-…-v2.0=; sb-…-v2.0=base64-REAL" -> [{ value: "base64-REAL" }] works
159
+ * "sb-…-v2.0=base64-REAL; sb-…-v2.0=" -> [{ value: "" }] anonymous
160
+ *
161
+ * That coin flip IS the production break, and it is invisible from the
162
+ * collapsed jar: reconciliation over `getAll()` is a no-op no matter how
163
+ * carefully it is written. Every door must therefore reconcile over THIS.
164
+ */
165
+ declare function parseCookieHeader(header: string | null | undefined): AuthCookieValue[];
166
+ declare function createAuthCookie(args: CreateAuthCookieArgs): AuthCookie;
167
+
168
+ export { type AuthCookie as A, type CreateAuthCookieArgs as C, DEFAULT_AUTH_COOKIE_NAME as D, type ReconciledAuthCookies as R, type AuthCookieOptions as a, type AuthCookieValue as b, createAuthCookie as c, parseCookieHeader as p };
@@ -0,0 +1,233 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/cookies.ts
21
+ var cookies_exports = {};
22
+ __export(cookies_exports, {
23
+ DEFAULT_ACTIVE_ORG_COOKIE_MAX_AGE: () => DEFAULT_ACTIVE_ORG_COOKIE_MAX_AGE,
24
+ DEFAULT_ACTIVE_ORG_COOKIE_NAME: () => DEFAULT_ACTIVE_ORG_COOKIE_NAME,
25
+ DEFAULT_AUTH_COOKIE_NAME: () => DEFAULT_AUTH_COOKIE_NAME,
26
+ createActiveOrgCookie: () => createActiveOrgCookie,
27
+ createAuthCookie: () => createAuthCookie,
28
+ decodeActiveOrgSelection: () => decodeActiveOrgSelection,
29
+ encodeActiveOrgSelection: () => encodeActiveOrgSelection,
30
+ parseCookieHeader: () => parseCookieHeader
31
+ });
32
+ module.exports = __toCommonJS(cookies_exports);
33
+
34
+ // src/cookies/auth-cookie.ts
35
+ var DEFAULT_AUTH_COOKIE_NAME = "sb-matrx-auth";
36
+ function parseCookieHeader(header) {
37
+ if (!header) return [];
38
+ const out = [];
39
+ for (const pair of header.split(";")) {
40
+ const eq = pair.indexOf("=");
41
+ const name = (eq === -1 ? pair : pair.slice(0, eq)).trim();
42
+ if (!name) continue;
43
+ const raw = eq === -1 ? "" : pair.slice(eq + 1).trim();
44
+ let value = raw;
45
+ try {
46
+ value = decodeURIComponent(raw);
47
+ } catch {
48
+ }
49
+ out.push({ name, value });
50
+ }
51
+ return out;
52
+ }
53
+ function belongsToStorageKey(name, storageKey) {
54
+ if (name === storageKey) return true;
55
+ if (!name.startsWith(`${storageKey}.`)) return false;
56
+ return /^(0|[1-9][0-9]*)$/.test(name.slice(storageKey.length + 1));
57
+ }
58
+ function renamedCookie(cookie, from, to) {
59
+ if (cookie.name === from) return { ...cookie, name: to };
60
+ const prefix = `${from}.`;
61
+ if (!cookie.name.startsWith(prefix)) return null;
62
+ const chunk = cookie.name.slice(prefix.length);
63
+ if (!/^(0|[1-9][0-9]*)$/.test(chunk)) return null;
64
+ return { ...cookie, name: `${to}.${chunk}` };
65
+ }
66
+ function createAuthCookie(args) {
67
+ const cookieName = args.cookieName ?? DEFAULT_AUTH_COOKIE_NAME;
68
+ const legacyCookieName = args.legacyCookieName;
69
+ const apex = args.apexDomain.toLowerCase();
70
+ const optionsForHost = (host) => {
71
+ const normalized = (host ?? "").toLowerCase();
72
+ const hostname = normalized.startsWith("[") ? normalized.slice(1, normalized.indexOf("]")) : normalized === "::1" ? normalized : normalized.split(":")[0] ?? "";
73
+ const loopback = hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "::1" || /^127(?:\.(?:0|[1-9]\d{0,2})){3}$/.test(hostname) && hostname.split(".").every((part) => Number(part) <= 255);
74
+ const secure = !loopback;
75
+ const onApex = hostname === apex || hostname.endsWith(`.${apex}`);
76
+ return onApex ? { name: cookieName, domain: `.${apex}`, secure } : { name: cookieName, secure };
77
+ };
78
+ const isCurrentCookie = (name) => belongsToStorageKey(name, cookieName);
79
+ const isAuthCookie = (name) => isCurrentCookie(name) || legacyCookieName !== void 0 && (name === legacyCookieName || name.startsWith(`${legacyCookieName}.`));
80
+ const reconcile = (cookies) => {
81
+ const seen = /* @__PURE__ */ new Map();
82
+ const out = [];
83
+ const shadowed = /* @__PURE__ */ new Set();
84
+ for (const cookie of cookies) {
85
+ const at = seen.get(cookie.name);
86
+ if (at === void 0) {
87
+ seen.set(cookie.name, out.length);
88
+ out.push({ name: cookie.name, value: cookie.value });
89
+ continue;
90
+ }
91
+ if (!isAuthCookie(cookie.name)) continue;
92
+ shadowed.add(cookie.name);
93
+ const held = out[at];
94
+ if (held !== void 0 && held.value.length === 0) {
95
+ out[at] = { name: cookie.name, value: cookie.value };
96
+ }
97
+ }
98
+ const ambiguous = /* @__PURE__ */ new Set();
99
+ const ambiguousKeys = [];
100
+ const byName = new Map(out.map((cookie) => [cookie.name, cookie]));
101
+ const storageKeys = [cookieName, ...legacyCookieName ? [legacyCookieName] : []];
102
+ for (const storageKey of storageKeys) {
103
+ const unchunked = byName.get(storageKey);
104
+ const firstChunk = byName.get(`${storageKey}.0`);
105
+ if (!unchunked?.value || !firstChunk?.value) continue;
106
+ ambiguousKeys.push(storageKey);
107
+ for (const { name } of out) {
108
+ if (belongsToStorageKey(name, storageKey)) ambiguous.add(name);
109
+ }
110
+ }
111
+ const resolved = ambiguous.size === 0 ? out : out.filter(({ name }) => !ambiguous.has(name));
112
+ const shadowedNames = [...shadowed];
113
+ const ambiguousNames = [...ambiguous];
114
+ return {
115
+ cookies: resolved,
116
+ shadowedNames,
117
+ ambiguousNames,
118
+ ambiguousKeys,
119
+ split: shadowedNames.length > 0 || ambiguousNames.length > 0
120
+ };
121
+ };
122
+ return {
123
+ cookieName,
124
+ legacyCookieName,
125
+ optionsForHost,
126
+ reconcile,
127
+ browserOptions: () => optionsForHost(
128
+ typeof window !== "undefined" ? window.location.hostname : null
129
+ ),
130
+ isCurrentCookie,
131
+ migrateLegacyCookies: (cookies) => {
132
+ if (!legacyCookieName) return [];
133
+ if (cookies.some(({ name }) => isCurrentCookie(name))) return [];
134
+ return cookies.flatMap((cookie) => {
135
+ const renamed = renamedCookie(cookie, legacyCookieName, cookieName);
136
+ return renamed ? [renamed] : [];
137
+ });
138
+ }
139
+ };
140
+ }
141
+
142
+ // src/cookies/active-org-cookie.ts
143
+ var import_uuid = require("@ai-matrx/kit/uuid");
144
+ var DEFAULT_ACTIVE_ORG_COOKIE_NAME = "matrx-active-org";
145
+ var DEFAULT_ACTIVE_ORG_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
146
+ function encodeActiveOrgSelection(selection) {
147
+ if (!(0, import_uuid.isUuidShape)(selection.userId) || !(0, import_uuid.isUuidShape)(selection.organizationId)) {
148
+ throw new Error(
149
+ "encodeActiveOrgSelection: userId and organizationId must both be UUIDs."
150
+ );
151
+ }
152
+ return `${selection.userId.toLowerCase()}:${selection.organizationId.toLowerCase()}`;
153
+ }
154
+ function decodeActiveOrgSelection(raw) {
155
+ if (!raw) return null;
156
+ const parts = raw.split(":");
157
+ if (parts.length !== 2) return null;
158
+ const [userId, organizationId] = parts;
159
+ if (!(0, import_uuid.isUuidShape)(userId) || !(0, import_uuid.isUuidShape)(organizationId)) return null;
160
+ return {
161
+ userId: userId.toLowerCase(),
162
+ organizationId: organizationId.toLowerCase()
163
+ };
164
+ }
165
+ function selectionFor(cookies, cookieName, userId) {
166
+ if (!userId) return null;
167
+ const wanted = userId.toLowerCase();
168
+ for (const cookie of cookies) {
169
+ if (cookie.name !== cookieName) continue;
170
+ const decoded = decodeActiveOrgSelection(cookie.value);
171
+ if (decoded && decoded.userId === wanted) return decoded.organizationId;
172
+ }
173
+ return null;
174
+ }
175
+ function attributes(options, maxAge, secure) {
176
+ const parts = [`Path=/`, `Max-Age=${maxAge}`, `SameSite=Lax`];
177
+ if (options.domain) parts.push(`Domain=${options.domain}`);
178
+ if (secure) parts.push("Secure");
179
+ return parts.join("; ");
180
+ }
181
+ function hasDocument() {
182
+ return typeof document !== "undefined" && typeof document.cookie === "string";
183
+ }
184
+ function createActiveOrgCookie(args) {
185
+ const cookieName = args.cookieName ?? DEFAULT_ACTIVE_ORG_COOKIE_NAME;
186
+ const maxAge = args.maxAgeSeconds ?? DEFAULT_ACTIVE_ORG_COOKIE_MAX_AGE;
187
+ const apexes = args.apexDomains.map((apex) => apex.toLowerCase());
188
+ const optionsForHost = (host) => {
189
+ const hostname = (host ?? "").split(":")[0]?.toLowerCase() ?? "";
190
+ const apex = apexes.find(
191
+ (candidate) => hostname === candidate || hostname.endsWith(`.${candidate}`)
192
+ );
193
+ return apex ? { name: cookieName, domain: `.${apex}` } : { name: cookieName };
194
+ };
195
+ const serialize = (host, selection, secure = false) => {
196
+ const options = optionsForHost(host);
197
+ if (!selection) {
198
+ return `${cookieName}=; ${attributes(options, 0, secure)}`;
199
+ }
200
+ return `${cookieName}=${encodeActiveOrgSelection(selection)}; ${attributes(options, maxAge, secure)}`;
201
+ };
202
+ const browserHost = () => typeof window !== "undefined" ? window.location.hostname : null;
203
+ const browserSecure = () => typeof window !== "undefined" && window.location.protocol === "https:";
204
+ const expireHostOnly = (secure) => {
205
+ document.cookie = `${cookieName}=; ${attributes({ name: cookieName }, 0, secure)}`;
206
+ };
207
+ return {
208
+ cookieName,
209
+ optionsForHost,
210
+ serialize,
211
+ readFromCookieHeader: (header, userId) => selectionFor(parseCookieHeader(header), cookieName, userId),
212
+ read: (userId) => {
213
+ if (!hasDocument()) return null;
214
+ return selectionFor(parseCookieHeader(document.cookie), cookieName, userId);
215
+ },
216
+ write: (selection) => {
217
+ if (!hasDocument()) return;
218
+ const host = browserHost();
219
+ const secure = browserSecure();
220
+ if (optionsForHost(host).domain) expireHostOnly(secure);
221
+ document.cookie = serialize(host, selection, secure);
222
+ },
223
+ clear: () => {
224
+ if (!hasDocument()) return;
225
+ const host = browserHost();
226
+ const secure = browserSecure();
227
+ expireHostOnly(secure);
228
+ if (optionsForHost(host).domain) {
229
+ document.cookie = serialize(host, null, secure);
230
+ }
231
+ }
232
+ };
233
+ }