@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.
- package/CHANGELOG.md +32 -0
- package/LICENSE +21 -0
- package/README.md +31 -1
- package/dist/auth-cookie-DWx6Q3xo.d.cts +168 -0
- package/dist/auth-cookie-DWx6Q3xo.d.ts +168 -0
- package/dist/cookies.cjs +233 -0
- package/dist/cookies.d.cts +109 -0
- package/dist/cookies.d.ts +109 -0
- package/dist/cookies.js +210 -0
- package/dist/credentials.cjs +68 -0
- package/dist/credentials.d.cts +82 -0
- package/dist/credentials.d.ts +82 -0
- package/dist/credentials.js +47 -0
- package/dist/index.cjs +189 -0
- package/dist/index.d.cts +86 -0
- package/dist/index.d.ts +86 -0
- package/dist/index.js +166 -0
- package/dist/next.cjs +655 -0
- package/dist/next.d.cts +389 -0
- package/dist/next.d.ts +389 -0
- package/dist/next.js +637 -0
- package/dist/node.cjs +61 -0
- package/dist/node.d.cts +33 -0
- package/dist/node.d.ts +33 -0
- package/dist/node.js +36 -0
- package/dist/organizations.cjs +389 -0
- package/dist/organizations.d.cts +226 -0
- package/dist/organizations.d.ts +226 -0
- package/dist/organizations.js +366 -0
- package/dist/react.cjs +206 -0
- package/dist/react.d.cts +163 -0
- package/dist/react.d.ts +163 -0
- package/dist/react.js +186 -0
- package/dist/session-DhRO4euL.d.cts +102 -0
- package/dist/session-DhRO4euL.d.ts +102 -0
- package/dist/session-identity.cjs +125 -0
- package/dist/session-identity.d.cts +100 -0
- package/dist/session-identity.d.ts +100 -0
- package/dist/session-identity.js +104 -0
- package/dist/testing.cjs +67 -0
- package/dist/testing.d.cts +38 -0
- package/dist/testing.d.ts +38 -0
- package/dist/testing.js +46 -0
- 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
|
-
|
|
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 };
|
package/dist/cookies.cjs
ADDED
|
@@ -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
|
+
}
|