@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
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
export { A as AuthCookie, a as AuthCookieOptions, b as AuthCookieValue, C as CreateAuthCookieArgs, D as DEFAULT_AUTH_COOKIE_NAME, R as ReconciledAuthCookies, c as createAuthCookie, p as parseCookieHeader } from './auth-cookie-DWx6Q3xo.cjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* THE ACTIVE-ORGANIZATION COOKIE — one browser-stored "which organization am I
|
|
5
|
+
* acting in" shared by EVERY Matrx web surface on the same apex.
|
|
6
|
+
*
|
|
7
|
+
* WHY (2026-09-08, the Workflow Studio `organization_not_selected` incident):
|
|
8
|
+
* the main app (www.aimatrx.com) remembered the last active organization in
|
|
9
|
+
* its own IndexedDB/localStorage cache; Workflow Studio (workflows.aimatrx.com)
|
|
10
|
+
* remembered its own choice under `matrx_studio_org`; the admin dashboard
|
|
11
|
+
* under `matrx_dashboard_org`. Three origins, three memories, and a person who
|
|
12
|
+
* had been working in one organization for weeks opened Studio and was
|
|
13
|
+
* refused on every request until they picked again — a choice they had
|
|
14
|
+
* already made. Browser storage is per-origin; a cookie issued at the apex
|
|
15
|
+
* (`Domain=.aimatrx.com`) is the ONE browser-stored value every subdomain can
|
|
16
|
+
* read, exactly how the platform already shares its login (see
|
|
17
|
+
* `auth-cookie.ts`). Cookies also ignore ports, so localhost:3000 and
|
|
18
|
+
* localhost:3101 share it in development.
|
|
19
|
+
*
|
|
20
|
+
* WHAT IT IS, AND IS NOT:
|
|
21
|
+
* - It is the FIRST rung of the canonical resolution order (stored selection
|
|
22
|
+
* → sole membership → make the user pick; a saved default is display-only;
|
|
23
|
+
* ruling: common-docs/projects/no-db-assigned-org/PLAN.md row EX-T05). A
|
|
24
|
+
* consumer still validates the stored id against live membership and
|
|
25
|
+
* drops it when the membership is gone — this module stores, it never
|
|
26
|
+
* decides.
|
|
27
|
+
* - It is IDENTITY-KEYED: the value carries the user id it was written for,
|
|
28
|
+
* and `read(userId)` answers null for anybody else. A second person
|
|
29
|
+
* signing in on the same browser must never inherit the first person's
|
|
30
|
+
* workspace (the same rule the sync engine applies to every persisted
|
|
31
|
+
* slice). Sign-out calls `clear()` on top of that.
|
|
32
|
+
* - It is NOT a security boundary. The server admits a request only when
|
|
33
|
+
* the `X-Organization-Id` it carries is a verified membership of the
|
|
34
|
+
* bearer; this cookie only remembers which of those the person chose.
|
|
35
|
+
* - It is NOT httpOnly — hosts read and write it from the browser. Nothing
|
|
36
|
+
* secret is in it.
|
|
37
|
+
*
|
|
38
|
+
* The Domain attribute is set ONLY on a real apex host (the same rule and the
|
|
39
|
+
* same reason as the auth cookie: a browser silently rejects a Set-Cookie
|
|
40
|
+
* whose Domain does not cover the current host). Studio runs on TWO apexes
|
|
41
|
+
* (workflows.aimatrx.com and studio.app.matrxserver.com), so this takes a
|
|
42
|
+
* list; the first apex that covers the host wins.
|
|
43
|
+
*/
|
|
44
|
+
declare const DEFAULT_ACTIVE_ORG_COOKIE_NAME = "matrx-active-org";
|
|
45
|
+
/** One year — the same horizon the platform's other preference cookies use. */
|
|
46
|
+
declare const DEFAULT_ACTIVE_ORG_COOKIE_MAX_AGE: number;
|
|
47
|
+
interface ActiveOrgCookieOptions {
|
|
48
|
+
name: string;
|
|
49
|
+
domain?: string;
|
|
50
|
+
}
|
|
51
|
+
/** The stored fact: WHO chose WHICH organization. */
|
|
52
|
+
interface ActiveOrgSelection {
|
|
53
|
+
userId: string;
|
|
54
|
+
organizationId: string;
|
|
55
|
+
}
|
|
56
|
+
interface CreateActiveOrgCookieArgs {
|
|
57
|
+
/**
|
|
58
|
+
* The apex domains one selection must span, e.g.
|
|
59
|
+
* `["aimatrx.com", "matrxserver.com"]`. Order matters only when two apexes
|
|
60
|
+
* could cover one host, which they never should.
|
|
61
|
+
*/
|
|
62
|
+
apexDomains: readonly string[];
|
|
63
|
+
/** Override only for a second, unrelated deployment — never per-app. */
|
|
64
|
+
cookieName?: string;
|
|
65
|
+
/** Lifetime in seconds. Defaults to one year. */
|
|
66
|
+
maxAgeSeconds?: number;
|
|
67
|
+
}
|
|
68
|
+
interface ActiveOrgCookie {
|
|
69
|
+
cookieName: string;
|
|
70
|
+
/**
|
|
71
|
+
* Cookie options for the host serving the current request.
|
|
72
|
+
* @param host `Host` header or `window.location.hostname` (port tolerated).
|
|
73
|
+
*/
|
|
74
|
+
optionsForHost(host: string | null | undefined): ActiveOrgCookieOptions;
|
|
75
|
+
/**
|
|
76
|
+
* The organization `userId` last chose, from a raw `Cookie` request header
|
|
77
|
+
* (SSR) — or null when nothing is stored, the value is malformed, or it
|
|
78
|
+
* belongs to a different person.
|
|
79
|
+
*/
|
|
80
|
+
readFromCookieHeader(header: string | null | undefined, userId: string | null | undefined): string | null;
|
|
81
|
+
/** Browser read of the same fact from `document.cookie`. SSR-safe (null). */
|
|
82
|
+
read(userId: string | null | undefined): string | null;
|
|
83
|
+
/**
|
|
84
|
+
* Record `userId`'s choice in the browser at the scope `optionsForHost`
|
|
85
|
+
* decides for this host. A previous host-only copy is expired first so the
|
|
86
|
+
* jar never holds two same-name cookies at two scopes (whichever the
|
|
87
|
+
* browser sent first would win — see `auth-cookie.ts` on split jars).
|
|
88
|
+
*/
|
|
89
|
+
write(selection: ActiveOrgSelection): void;
|
|
90
|
+
/**
|
|
91
|
+
* Forget the selection in the browser — at EVERY scope this host could have
|
|
92
|
+
* issued it (domain-wide and host-only), so sign-out cannot leave a copy
|
|
93
|
+
* behind for the next person.
|
|
94
|
+
*/
|
|
95
|
+
clear(): void;
|
|
96
|
+
/**
|
|
97
|
+
* A `Set-Cookie` header value recording `selection` for `host` (null →
|
|
98
|
+
* an expiring cookie). For servers that want to write it; browsers use
|
|
99
|
+
* `write`.
|
|
100
|
+
*/
|
|
101
|
+
serialize(host: string | null | undefined, selection: ActiveOrgSelection | null, secure?: boolean): string;
|
|
102
|
+
}
|
|
103
|
+
/** `<userId>:<organizationId>` — two UUIDs, one separator. */
|
|
104
|
+
declare function encodeActiveOrgSelection(selection: ActiveOrgSelection): string;
|
|
105
|
+
/** Inverse of {@link encodeActiveOrgSelection}. Null for anything malformed. */
|
|
106
|
+
declare function decodeActiveOrgSelection(raw: string | null | undefined): ActiveOrgSelection | null;
|
|
107
|
+
declare function createActiveOrgCookie(args: CreateActiveOrgCookieArgs): ActiveOrgCookie;
|
|
108
|
+
|
|
109
|
+
export { type ActiveOrgCookie, type ActiveOrgCookieOptions, type ActiveOrgSelection, type CreateActiveOrgCookieArgs, DEFAULT_ACTIVE_ORG_COOKIE_MAX_AGE, DEFAULT_ACTIVE_ORG_COOKIE_NAME, createActiveOrgCookie, decodeActiveOrgSelection, encodeActiveOrgSelection };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
export { A as AuthCookie, a as AuthCookieOptions, b as AuthCookieValue, C as CreateAuthCookieArgs, D as DEFAULT_AUTH_COOKIE_NAME, R as ReconciledAuthCookies, c as createAuthCookie, p as parseCookieHeader } from './auth-cookie-DWx6Q3xo.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* THE ACTIVE-ORGANIZATION COOKIE — one browser-stored "which organization am I
|
|
5
|
+
* acting in" shared by EVERY Matrx web surface on the same apex.
|
|
6
|
+
*
|
|
7
|
+
* WHY (2026-09-08, the Workflow Studio `organization_not_selected` incident):
|
|
8
|
+
* the main app (www.aimatrx.com) remembered the last active organization in
|
|
9
|
+
* its own IndexedDB/localStorage cache; Workflow Studio (workflows.aimatrx.com)
|
|
10
|
+
* remembered its own choice under `matrx_studio_org`; the admin dashboard
|
|
11
|
+
* under `matrx_dashboard_org`. Three origins, three memories, and a person who
|
|
12
|
+
* had been working in one organization for weeks opened Studio and was
|
|
13
|
+
* refused on every request until they picked again — a choice they had
|
|
14
|
+
* already made. Browser storage is per-origin; a cookie issued at the apex
|
|
15
|
+
* (`Domain=.aimatrx.com`) is the ONE browser-stored value every subdomain can
|
|
16
|
+
* read, exactly how the platform already shares its login (see
|
|
17
|
+
* `auth-cookie.ts`). Cookies also ignore ports, so localhost:3000 and
|
|
18
|
+
* localhost:3101 share it in development.
|
|
19
|
+
*
|
|
20
|
+
* WHAT IT IS, AND IS NOT:
|
|
21
|
+
* - It is the FIRST rung of the canonical resolution order (stored selection
|
|
22
|
+
* → sole membership → make the user pick; a saved default is display-only;
|
|
23
|
+
* ruling: common-docs/projects/no-db-assigned-org/PLAN.md row EX-T05). A
|
|
24
|
+
* consumer still validates the stored id against live membership and
|
|
25
|
+
* drops it when the membership is gone — this module stores, it never
|
|
26
|
+
* decides.
|
|
27
|
+
* - It is IDENTITY-KEYED: the value carries the user id it was written for,
|
|
28
|
+
* and `read(userId)` answers null for anybody else. A second person
|
|
29
|
+
* signing in on the same browser must never inherit the first person's
|
|
30
|
+
* workspace (the same rule the sync engine applies to every persisted
|
|
31
|
+
* slice). Sign-out calls `clear()` on top of that.
|
|
32
|
+
* - It is NOT a security boundary. The server admits a request only when
|
|
33
|
+
* the `X-Organization-Id` it carries is a verified membership of the
|
|
34
|
+
* bearer; this cookie only remembers which of those the person chose.
|
|
35
|
+
* - It is NOT httpOnly — hosts read and write it from the browser. Nothing
|
|
36
|
+
* secret is in it.
|
|
37
|
+
*
|
|
38
|
+
* The Domain attribute is set ONLY on a real apex host (the same rule and the
|
|
39
|
+
* same reason as the auth cookie: a browser silently rejects a Set-Cookie
|
|
40
|
+
* whose Domain does not cover the current host). Studio runs on TWO apexes
|
|
41
|
+
* (workflows.aimatrx.com and studio.app.matrxserver.com), so this takes a
|
|
42
|
+
* list; the first apex that covers the host wins.
|
|
43
|
+
*/
|
|
44
|
+
declare const DEFAULT_ACTIVE_ORG_COOKIE_NAME = "matrx-active-org";
|
|
45
|
+
/** One year — the same horizon the platform's other preference cookies use. */
|
|
46
|
+
declare const DEFAULT_ACTIVE_ORG_COOKIE_MAX_AGE: number;
|
|
47
|
+
interface ActiveOrgCookieOptions {
|
|
48
|
+
name: string;
|
|
49
|
+
domain?: string;
|
|
50
|
+
}
|
|
51
|
+
/** The stored fact: WHO chose WHICH organization. */
|
|
52
|
+
interface ActiveOrgSelection {
|
|
53
|
+
userId: string;
|
|
54
|
+
organizationId: string;
|
|
55
|
+
}
|
|
56
|
+
interface CreateActiveOrgCookieArgs {
|
|
57
|
+
/**
|
|
58
|
+
* The apex domains one selection must span, e.g.
|
|
59
|
+
* `["aimatrx.com", "matrxserver.com"]`. Order matters only when two apexes
|
|
60
|
+
* could cover one host, which they never should.
|
|
61
|
+
*/
|
|
62
|
+
apexDomains: readonly string[];
|
|
63
|
+
/** Override only for a second, unrelated deployment — never per-app. */
|
|
64
|
+
cookieName?: string;
|
|
65
|
+
/** Lifetime in seconds. Defaults to one year. */
|
|
66
|
+
maxAgeSeconds?: number;
|
|
67
|
+
}
|
|
68
|
+
interface ActiveOrgCookie {
|
|
69
|
+
cookieName: string;
|
|
70
|
+
/**
|
|
71
|
+
* Cookie options for the host serving the current request.
|
|
72
|
+
* @param host `Host` header or `window.location.hostname` (port tolerated).
|
|
73
|
+
*/
|
|
74
|
+
optionsForHost(host: string | null | undefined): ActiveOrgCookieOptions;
|
|
75
|
+
/**
|
|
76
|
+
* The organization `userId` last chose, from a raw `Cookie` request header
|
|
77
|
+
* (SSR) — or null when nothing is stored, the value is malformed, or it
|
|
78
|
+
* belongs to a different person.
|
|
79
|
+
*/
|
|
80
|
+
readFromCookieHeader(header: string | null | undefined, userId: string | null | undefined): string | null;
|
|
81
|
+
/** Browser read of the same fact from `document.cookie`. SSR-safe (null). */
|
|
82
|
+
read(userId: string | null | undefined): string | null;
|
|
83
|
+
/**
|
|
84
|
+
* Record `userId`'s choice in the browser at the scope `optionsForHost`
|
|
85
|
+
* decides for this host. A previous host-only copy is expired first so the
|
|
86
|
+
* jar never holds two same-name cookies at two scopes (whichever the
|
|
87
|
+
* browser sent first would win — see `auth-cookie.ts` on split jars).
|
|
88
|
+
*/
|
|
89
|
+
write(selection: ActiveOrgSelection): void;
|
|
90
|
+
/**
|
|
91
|
+
* Forget the selection in the browser — at EVERY scope this host could have
|
|
92
|
+
* issued it (domain-wide and host-only), so sign-out cannot leave a copy
|
|
93
|
+
* behind for the next person.
|
|
94
|
+
*/
|
|
95
|
+
clear(): void;
|
|
96
|
+
/**
|
|
97
|
+
* A `Set-Cookie` header value recording `selection` for `host` (null →
|
|
98
|
+
* an expiring cookie). For servers that want to write it; browsers use
|
|
99
|
+
* `write`.
|
|
100
|
+
*/
|
|
101
|
+
serialize(host: string | null | undefined, selection: ActiveOrgSelection | null, secure?: boolean): string;
|
|
102
|
+
}
|
|
103
|
+
/** `<userId>:<organizationId>` — two UUIDs, one separator. */
|
|
104
|
+
declare function encodeActiveOrgSelection(selection: ActiveOrgSelection): string;
|
|
105
|
+
/** Inverse of {@link encodeActiveOrgSelection}. Null for anything malformed. */
|
|
106
|
+
declare function decodeActiveOrgSelection(raw: string | null | undefined): ActiveOrgSelection | null;
|
|
107
|
+
declare function createActiveOrgCookie(args: CreateActiveOrgCookieArgs): ActiveOrgCookie;
|
|
108
|
+
|
|
109
|
+
export { type ActiveOrgCookie, type ActiveOrgCookieOptions, type ActiveOrgSelection, type CreateActiveOrgCookieArgs, DEFAULT_ACTIVE_ORG_COOKIE_MAX_AGE, DEFAULT_ACTIVE_ORG_COOKIE_NAME, createActiveOrgCookie, decodeActiveOrgSelection, encodeActiveOrgSelection };
|
package/dist/cookies.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// src/cookies/auth-cookie.ts
|
|
2
|
+
var DEFAULT_AUTH_COOKIE_NAME = "sb-matrx-auth";
|
|
3
|
+
function parseCookieHeader(header) {
|
|
4
|
+
if (!header) return [];
|
|
5
|
+
const out = [];
|
|
6
|
+
for (const pair of header.split(";")) {
|
|
7
|
+
const eq = pair.indexOf("=");
|
|
8
|
+
const name = (eq === -1 ? pair : pair.slice(0, eq)).trim();
|
|
9
|
+
if (!name) continue;
|
|
10
|
+
const raw = eq === -1 ? "" : pair.slice(eq + 1).trim();
|
|
11
|
+
let value = raw;
|
|
12
|
+
try {
|
|
13
|
+
value = decodeURIComponent(raw);
|
|
14
|
+
} catch {
|
|
15
|
+
}
|
|
16
|
+
out.push({ name, value });
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
function belongsToStorageKey(name, storageKey) {
|
|
21
|
+
if (name === storageKey) return true;
|
|
22
|
+
if (!name.startsWith(`${storageKey}.`)) return false;
|
|
23
|
+
return /^(0|[1-9][0-9]*)$/.test(name.slice(storageKey.length + 1));
|
|
24
|
+
}
|
|
25
|
+
function renamedCookie(cookie, from, to) {
|
|
26
|
+
if (cookie.name === from) return { ...cookie, name: to };
|
|
27
|
+
const prefix = `${from}.`;
|
|
28
|
+
if (!cookie.name.startsWith(prefix)) return null;
|
|
29
|
+
const chunk = cookie.name.slice(prefix.length);
|
|
30
|
+
if (!/^(0|[1-9][0-9]*)$/.test(chunk)) return null;
|
|
31
|
+
return { ...cookie, name: `${to}.${chunk}` };
|
|
32
|
+
}
|
|
33
|
+
function createAuthCookie(args) {
|
|
34
|
+
const cookieName = args.cookieName ?? DEFAULT_AUTH_COOKIE_NAME;
|
|
35
|
+
const legacyCookieName = args.legacyCookieName;
|
|
36
|
+
const apex = args.apexDomain.toLowerCase();
|
|
37
|
+
const optionsForHost = (host) => {
|
|
38
|
+
const normalized = (host ?? "").toLowerCase();
|
|
39
|
+
const hostname = normalized.startsWith("[") ? normalized.slice(1, normalized.indexOf("]")) : normalized === "::1" ? normalized : normalized.split(":")[0] ?? "";
|
|
40
|
+
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);
|
|
41
|
+
const secure = !loopback;
|
|
42
|
+
const onApex = hostname === apex || hostname.endsWith(`.${apex}`);
|
|
43
|
+
return onApex ? { name: cookieName, domain: `.${apex}`, secure } : { name: cookieName, secure };
|
|
44
|
+
};
|
|
45
|
+
const isCurrentCookie = (name) => belongsToStorageKey(name, cookieName);
|
|
46
|
+
const isAuthCookie = (name) => isCurrentCookie(name) || legacyCookieName !== void 0 && (name === legacyCookieName || name.startsWith(`${legacyCookieName}.`));
|
|
47
|
+
const reconcile = (cookies) => {
|
|
48
|
+
const seen = /* @__PURE__ */ new Map();
|
|
49
|
+
const out = [];
|
|
50
|
+
const shadowed = /* @__PURE__ */ new Set();
|
|
51
|
+
for (const cookie of cookies) {
|
|
52
|
+
const at = seen.get(cookie.name);
|
|
53
|
+
if (at === void 0) {
|
|
54
|
+
seen.set(cookie.name, out.length);
|
|
55
|
+
out.push({ name: cookie.name, value: cookie.value });
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (!isAuthCookie(cookie.name)) continue;
|
|
59
|
+
shadowed.add(cookie.name);
|
|
60
|
+
const held = out[at];
|
|
61
|
+
if (held !== void 0 && held.value.length === 0) {
|
|
62
|
+
out[at] = { name: cookie.name, value: cookie.value };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const ambiguous = /* @__PURE__ */ new Set();
|
|
66
|
+
const ambiguousKeys = [];
|
|
67
|
+
const byName = new Map(out.map((cookie) => [cookie.name, cookie]));
|
|
68
|
+
const storageKeys = [cookieName, ...legacyCookieName ? [legacyCookieName] : []];
|
|
69
|
+
for (const storageKey of storageKeys) {
|
|
70
|
+
const unchunked = byName.get(storageKey);
|
|
71
|
+
const firstChunk = byName.get(`${storageKey}.0`);
|
|
72
|
+
if (!unchunked?.value || !firstChunk?.value) continue;
|
|
73
|
+
ambiguousKeys.push(storageKey);
|
|
74
|
+
for (const { name } of out) {
|
|
75
|
+
if (belongsToStorageKey(name, storageKey)) ambiguous.add(name);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const resolved = ambiguous.size === 0 ? out : out.filter(({ name }) => !ambiguous.has(name));
|
|
79
|
+
const shadowedNames = [...shadowed];
|
|
80
|
+
const ambiguousNames = [...ambiguous];
|
|
81
|
+
return {
|
|
82
|
+
cookies: resolved,
|
|
83
|
+
shadowedNames,
|
|
84
|
+
ambiguousNames,
|
|
85
|
+
ambiguousKeys,
|
|
86
|
+
split: shadowedNames.length > 0 || ambiguousNames.length > 0
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
return {
|
|
90
|
+
cookieName,
|
|
91
|
+
legacyCookieName,
|
|
92
|
+
optionsForHost,
|
|
93
|
+
reconcile,
|
|
94
|
+
browserOptions: () => optionsForHost(
|
|
95
|
+
typeof window !== "undefined" ? window.location.hostname : null
|
|
96
|
+
),
|
|
97
|
+
isCurrentCookie,
|
|
98
|
+
migrateLegacyCookies: (cookies) => {
|
|
99
|
+
if (!legacyCookieName) return [];
|
|
100
|
+
if (cookies.some(({ name }) => isCurrentCookie(name))) return [];
|
|
101
|
+
return cookies.flatMap((cookie) => {
|
|
102
|
+
const renamed = renamedCookie(cookie, legacyCookieName, cookieName);
|
|
103
|
+
return renamed ? [renamed] : [];
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/cookies/active-org-cookie.ts
|
|
110
|
+
import { isUuidShape } from "@ai-matrx/kit/uuid";
|
|
111
|
+
var DEFAULT_ACTIVE_ORG_COOKIE_NAME = "matrx-active-org";
|
|
112
|
+
var DEFAULT_ACTIVE_ORG_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
|
|
113
|
+
function encodeActiveOrgSelection(selection) {
|
|
114
|
+
if (!isUuidShape(selection.userId) || !isUuidShape(selection.organizationId)) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
"encodeActiveOrgSelection: userId and organizationId must both be UUIDs."
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return `${selection.userId.toLowerCase()}:${selection.organizationId.toLowerCase()}`;
|
|
120
|
+
}
|
|
121
|
+
function decodeActiveOrgSelection(raw) {
|
|
122
|
+
if (!raw) return null;
|
|
123
|
+
const parts = raw.split(":");
|
|
124
|
+
if (parts.length !== 2) return null;
|
|
125
|
+
const [userId, organizationId] = parts;
|
|
126
|
+
if (!isUuidShape(userId) || !isUuidShape(organizationId)) return null;
|
|
127
|
+
return {
|
|
128
|
+
userId: userId.toLowerCase(),
|
|
129
|
+
organizationId: organizationId.toLowerCase()
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function selectionFor(cookies, cookieName, userId) {
|
|
133
|
+
if (!userId) return null;
|
|
134
|
+
const wanted = userId.toLowerCase();
|
|
135
|
+
for (const cookie of cookies) {
|
|
136
|
+
if (cookie.name !== cookieName) continue;
|
|
137
|
+
const decoded = decodeActiveOrgSelection(cookie.value);
|
|
138
|
+
if (decoded && decoded.userId === wanted) return decoded.organizationId;
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
function attributes(options, maxAge, secure) {
|
|
143
|
+
const parts = [`Path=/`, `Max-Age=${maxAge}`, `SameSite=Lax`];
|
|
144
|
+
if (options.domain) parts.push(`Domain=${options.domain}`);
|
|
145
|
+
if (secure) parts.push("Secure");
|
|
146
|
+
return parts.join("; ");
|
|
147
|
+
}
|
|
148
|
+
function hasDocument() {
|
|
149
|
+
return typeof document !== "undefined" && typeof document.cookie === "string";
|
|
150
|
+
}
|
|
151
|
+
function createActiveOrgCookie(args) {
|
|
152
|
+
const cookieName = args.cookieName ?? DEFAULT_ACTIVE_ORG_COOKIE_NAME;
|
|
153
|
+
const maxAge = args.maxAgeSeconds ?? DEFAULT_ACTIVE_ORG_COOKIE_MAX_AGE;
|
|
154
|
+
const apexes = args.apexDomains.map((apex) => apex.toLowerCase());
|
|
155
|
+
const optionsForHost = (host) => {
|
|
156
|
+
const hostname = (host ?? "").split(":")[0]?.toLowerCase() ?? "";
|
|
157
|
+
const apex = apexes.find(
|
|
158
|
+
(candidate) => hostname === candidate || hostname.endsWith(`.${candidate}`)
|
|
159
|
+
);
|
|
160
|
+
return apex ? { name: cookieName, domain: `.${apex}` } : { name: cookieName };
|
|
161
|
+
};
|
|
162
|
+
const serialize = (host, selection, secure = false) => {
|
|
163
|
+
const options = optionsForHost(host);
|
|
164
|
+
if (!selection) {
|
|
165
|
+
return `${cookieName}=; ${attributes(options, 0, secure)}`;
|
|
166
|
+
}
|
|
167
|
+
return `${cookieName}=${encodeActiveOrgSelection(selection)}; ${attributes(options, maxAge, secure)}`;
|
|
168
|
+
};
|
|
169
|
+
const browserHost = () => typeof window !== "undefined" ? window.location.hostname : null;
|
|
170
|
+
const browserSecure = () => typeof window !== "undefined" && window.location.protocol === "https:";
|
|
171
|
+
const expireHostOnly = (secure) => {
|
|
172
|
+
document.cookie = `${cookieName}=; ${attributes({ name: cookieName }, 0, secure)}`;
|
|
173
|
+
};
|
|
174
|
+
return {
|
|
175
|
+
cookieName,
|
|
176
|
+
optionsForHost,
|
|
177
|
+
serialize,
|
|
178
|
+
readFromCookieHeader: (header, userId) => selectionFor(parseCookieHeader(header), cookieName, userId),
|
|
179
|
+
read: (userId) => {
|
|
180
|
+
if (!hasDocument()) return null;
|
|
181
|
+
return selectionFor(parseCookieHeader(document.cookie), cookieName, userId);
|
|
182
|
+
},
|
|
183
|
+
write: (selection) => {
|
|
184
|
+
if (!hasDocument()) return;
|
|
185
|
+
const host = browserHost();
|
|
186
|
+
const secure = browserSecure();
|
|
187
|
+
if (optionsForHost(host).domain) expireHostOnly(secure);
|
|
188
|
+
document.cookie = serialize(host, selection, secure);
|
|
189
|
+
},
|
|
190
|
+
clear: () => {
|
|
191
|
+
if (!hasDocument()) return;
|
|
192
|
+
const host = browserHost();
|
|
193
|
+
const secure = browserSecure();
|
|
194
|
+
expireHostOnly(secure);
|
|
195
|
+
if (optionsForHost(host).domain) {
|
|
196
|
+
document.cookie = serialize(host, null, secure);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
export {
|
|
202
|
+
DEFAULT_ACTIVE_ORG_COOKIE_MAX_AGE,
|
|
203
|
+
DEFAULT_ACTIVE_ORG_COOKIE_NAME,
|
|
204
|
+
DEFAULT_AUTH_COOKIE_NAME,
|
|
205
|
+
createActiveOrgCookie,
|
|
206
|
+
createAuthCookie,
|
|
207
|
+
decodeActiveOrgSelection,
|
|
208
|
+
encodeActiveOrgSelection,
|
|
209
|
+
parseCookieHeader
|
|
210
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
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/credentials.ts
|
|
21
|
+
var credentials_exports = {};
|
|
22
|
+
__export(credentials_exports, {
|
|
23
|
+
API_KEY_PREFIX: () => API_KEY_PREFIX,
|
|
24
|
+
MATRX_HEADERS: () => MATRX_HEADERS,
|
|
25
|
+
credentialToHeaders: () => credentialToHeaders,
|
|
26
|
+
toCredentialsPort: () => toCredentialsPort
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(credentials_exports);
|
|
29
|
+
var MATRX_HEADERS = {
|
|
30
|
+
authorization: "Authorization",
|
|
31
|
+
fingerprint: "X-Fingerprint-ID",
|
|
32
|
+
organization: "X-Organization-Id",
|
|
33
|
+
actingUser: "X-Matrx-User-Id",
|
|
34
|
+
requestId: "X-Request-Id"
|
|
35
|
+
};
|
|
36
|
+
var API_KEY_PREFIX = "mx_";
|
|
37
|
+
function credentialToHeaders(credential) {
|
|
38
|
+
if (credential === null) return {};
|
|
39
|
+
switch (credential.kind) {
|
|
40
|
+
case "user":
|
|
41
|
+
return { [MATRX_HEADERS.authorization]: `Bearer ${credential.accessToken}` };
|
|
42
|
+
case "guest":
|
|
43
|
+
return { [MATRX_HEADERS.fingerprint]: credential.fingerprintId };
|
|
44
|
+
case "scoped":
|
|
45
|
+
return { [MATRX_HEADERS.authorization]: `Bearer ${credential.token}` };
|
|
46
|
+
case "service": {
|
|
47
|
+
const header = credential.secretHeader ?? MATRX_HEADERS.authorization;
|
|
48
|
+
const headers = header.toLowerCase() === "authorization" ? { [MATRX_HEADERS.authorization]: `Bearer ${credential.secret}` } : { [header]: credential.secret };
|
|
49
|
+
if (credential.actingUserId) headers[MATRX_HEADERS.actingUser] = credential.actingUserId;
|
|
50
|
+
return headers;
|
|
51
|
+
}
|
|
52
|
+
case "api_key": {
|
|
53
|
+
if (!credential.apiKey.startsWith(API_KEY_PREFIX)) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`credentialToHeaders: an api_key credential must start with "${API_KEY_PREFIX}" (AI Matrx API keys look like mx_live_...)`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return { [MATRX_HEADERS.authorization]: `Bearer ${credential.apiKey}` };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function toCredentialsPort(setting) {
|
|
63
|
+
if (setting === void 0 || setting === null) return { get: async () => null };
|
|
64
|
+
if (typeof setting === "function") return { get: async () => setting() };
|
|
65
|
+
if ("get" in setting && typeof setting.get === "function") return setting;
|
|
66
|
+
const fixed = setting;
|
|
67
|
+
return { get: async () => fixed };
|
|
68
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The credentials port — the ONE credential→header mapping for every client
|
|
3
|
+
* of every AI Matrx server (package lines v3 §3.4; API-key lane C16).
|
|
4
|
+
*
|
|
5
|
+
* Five credential kinds, one mapping, in exactly one place. The kind names are
|
|
6
|
+
* the contract's own `x-matrx-auth` tier names, so a generated operation's
|
|
7
|
+
* tier and the credential that satisfies it read the same in every language
|
|
8
|
+
* (TypeScript here; the Python `matrx-api` and future Swift/Kotlin clients
|
|
9
|
+
* share these header names and shapes — see {@link MATRX_HEADERS}):
|
|
10
|
+
*
|
|
11
|
+
* - `user` — a signed-in person: `Authorization: Bearer <Supabase JWT>`.
|
|
12
|
+
* - `guest` — a fingerprint: `X-Fingerprint-ID: <id>`.
|
|
13
|
+
* - `api_key` — an AI Matrx API key: `Authorization: Bearer mx_live_...`.
|
|
14
|
+
* The server's ONE AuthMiddleware disambiguates by shape (`mx_` prefix vs
|
|
15
|
+
* three-dot JWT), so `user` and `api_key` ride the same header.
|
|
16
|
+
* - `service` — one of our servers calling another: the server secret as
|
|
17
|
+
* `Authorization: Bearer <secret>` (or in `secretHeader`), plus the acting
|
|
18
|
+
* person in `X-Matrx-User-Id` when there is one
|
|
19
|
+
* (`matrx_connect.service_auth`).
|
|
20
|
+
* - `scoped` — a short-lived capability token (sandbox, desktop app, broker
|
|
21
|
+
* client): `Authorization: Bearer <token>` (`matrx_connect.scoped_tokens`).
|
|
22
|
+
* Short-lived means the PORT re-reads it per call: hand a `CredentialsPort`
|
|
23
|
+
* whose `get()` returns the current token.
|
|
24
|
+
*
|
|
25
|
+
* No polling, no readiness loop: readiness is the port's promise.
|
|
26
|
+
*/
|
|
27
|
+
/** Header names every AI Matrx client and server shares (TS, Python, native). */
|
|
28
|
+
declare const MATRX_HEADERS: {
|
|
29
|
+
readonly authorization: "Authorization";
|
|
30
|
+
readonly fingerprint: "X-Fingerprint-ID";
|
|
31
|
+
readonly organization: "X-Organization-Id";
|
|
32
|
+
readonly actingUser: "X-Matrx-User-Id";
|
|
33
|
+
readonly requestId: "X-Request-Id";
|
|
34
|
+
};
|
|
35
|
+
/** The credential kinds — identical to the contract's `x-matrx-auth` tiers that carry a credential. */
|
|
36
|
+
type MatrxCredentialKind = MatrxCredential["kind"];
|
|
37
|
+
/** A credential a Matrx client can present. */
|
|
38
|
+
type MatrxCredential = {
|
|
39
|
+
kind: "user";
|
|
40
|
+
accessToken: string;
|
|
41
|
+
} | {
|
|
42
|
+
kind: "guest";
|
|
43
|
+
fingerprintId: string;
|
|
44
|
+
} | {
|
|
45
|
+
kind: "api_key";
|
|
46
|
+
apiKey: string;
|
|
47
|
+
} | {
|
|
48
|
+
kind: "service";
|
|
49
|
+
/** The server secret (an existing env value, e.g. MATRX_AIDREAM_SERVICE_TOKEN). */
|
|
50
|
+
secret: string;
|
|
51
|
+
/** The person the call acts for, when there is one (`X-Matrx-User-Id`). */
|
|
52
|
+
actingUserId?: string;
|
|
53
|
+
/** Where the secret rides. Default `Authorization` (as a Bearer value). */
|
|
54
|
+
secretHeader?: string;
|
|
55
|
+
} | {
|
|
56
|
+
kind: "scoped";
|
|
57
|
+
token: string;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* The async source of the current credential. `get()` resolves when the host
|
|
61
|
+
* knows its auth state (null = anonymous); `onChange` lets transports refresh
|
|
62
|
+
* cached headers when the credential rotates.
|
|
63
|
+
*/
|
|
64
|
+
interface CredentialsPort {
|
|
65
|
+
get(): Promise<MatrxCredential | null>;
|
|
66
|
+
onChange?(cb: () => void): () => void;
|
|
67
|
+
}
|
|
68
|
+
/** Prefix that marks a bearer token as an AI Matrx API key, never a JWT. */
|
|
69
|
+
declare const API_KEY_PREFIX = "mx_";
|
|
70
|
+
/**
|
|
71
|
+
* Map a credential to the exact request headers the AI Matrx platform expects.
|
|
72
|
+
* This is the single place that knowledge lives — transports spread the result
|
|
73
|
+
* into their request headers and add `X-Organization-Id` from the org context
|
|
74
|
+
* binding separately.
|
|
75
|
+
*/
|
|
76
|
+
declare function credentialToHeaders(credential: MatrxCredential | null): Record<string, string>;
|
|
77
|
+
/** What `createApi({ credentials })` accepts: a fixed credential, a getter, or a port. */
|
|
78
|
+
type CredentialsSetting = MatrxCredential | null | (() => MatrxCredential | null | Promise<MatrxCredential | null>) | CredentialsPort;
|
|
79
|
+
/** Normalize the ONE credentials setting into a `CredentialsPort`. */
|
|
80
|
+
declare function toCredentialsPort(setting: CredentialsSetting | undefined): CredentialsPort;
|
|
81
|
+
|
|
82
|
+
export { API_KEY_PREFIX, type CredentialsPort, type CredentialsSetting, MATRX_HEADERS, type MatrxCredential, type MatrxCredentialKind, credentialToHeaders, toCredentialsPort };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The credentials port — the ONE credential→header mapping for every client
|
|
3
|
+
* of every AI Matrx server (package lines v3 §3.4; API-key lane C16).
|
|
4
|
+
*
|
|
5
|
+
* Five credential kinds, one mapping, in exactly one place. The kind names are
|
|
6
|
+
* the contract's own `x-matrx-auth` tier names, so a generated operation's
|
|
7
|
+
* tier and the credential that satisfies it read the same in every language
|
|
8
|
+
* (TypeScript here; the Python `matrx-api` and future Swift/Kotlin clients
|
|
9
|
+
* share these header names and shapes — see {@link MATRX_HEADERS}):
|
|
10
|
+
*
|
|
11
|
+
* - `user` — a signed-in person: `Authorization: Bearer <Supabase JWT>`.
|
|
12
|
+
* - `guest` — a fingerprint: `X-Fingerprint-ID: <id>`.
|
|
13
|
+
* - `api_key` — an AI Matrx API key: `Authorization: Bearer mx_live_...`.
|
|
14
|
+
* The server's ONE AuthMiddleware disambiguates by shape (`mx_` prefix vs
|
|
15
|
+
* three-dot JWT), so `user` and `api_key` ride the same header.
|
|
16
|
+
* - `service` — one of our servers calling another: the server secret as
|
|
17
|
+
* `Authorization: Bearer <secret>` (or in `secretHeader`), plus the acting
|
|
18
|
+
* person in `X-Matrx-User-Id` when there is one
|
|
19
|
+
* (`matrx_connect.service_auth`).
|
|
20
|
+
* - `scoped` — a short-lived capability token (sandbox, desktop app, broker
|
|
21
|
+
* client): `Authorization: Bearer <token>` (`matrx_connect.scoped_tokens`).
|
|
22
|
+
* Short-lived means the PORT re-reads it per call: hand a `CredentialsPort`
|
|
23
|
+
* whose `get()` returns the current token.
|
|
24
|
+
*
|
|
25
|
+
* No polling, no readiness loop: readiness is the port's promise.
|
|
26
|
+
*/
|
|
27
|
+
/** Header names every AI Matrx client and server shares (TS, Python, native). */
|
|
28
|
+
declare const MATRX_HEADERS: {
|
|
29
|
+
readonly authorization: "Authorization";
|
|
30
|
+
readonly fingerprint: "X-Fingerprint-ID";
|
|
31
|
+
readonly organization: "X-Organization-Id";
|
|
32
|
+
readonly actingUser: "X-Matrx-User-Id";
|
|
33
|
+
readonly requestId: "X-Request-Id";
|
|
34
|
+
};
|
|
35
|
+
/** The credential kinds — identical to the contract's `x-matrx-auth` tiers that carry a credential. */
|
|
36
|
+
type MatrxCredentialKind = MatrxCredential["kind"];
|
|
37
|
+
/** A credential a Matrx client can present. */
|
|
38
|
+
type MatrxCredential = {
|
|
39
|
+
kind: "user";
|
|
40
|
+
accessToken: string;
|
|
41
|
+
} | {
|
|
42
|
+
kind: "guest";
|
|
43
|
+
fingerprintId: string;
|
|
44
|
+
} | {
|
|
45
|
+
kind: "api_key";
|
|
46
|
+
apiKey: string;
|
|
47
|
+
} | {
|
|
48
|
+
kind: "service";
|
|
49
|
+
/** The server secret (an existing env value, e.g. MATRX_AIDREAM_SERVICE_TOKEN). */
|
|
50
|
+
secret: string;
|
|
51
|
+
/** The person the call acts for, when there is one (`X-Matrx-User-Id`). */
|
|
52
|
+
actingUserId?: string;
|
|
53
|
+
/** Where the secret rides. Default `Authorization` (as a Bearer value). */
|
|
54
|
+
secretHeader?: string;
|
|
55
|
+
} | {
|
|
56
|
+
kind: "scoped";
|
|
57
|
+
token: string;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* The async source of the current credential. `get()` resolves when the host
|
|
61
|
+
* knows its auth state (null = anonymous); `onChange` lets transports refresh
|
|
62
|
+
* cached headers when the credential rotates.
|
|
63
|
+
*/
|
|
64
|
+
interface CredentialsPort {
|
|
65
|
+
get(): Promise<MatrxCredential | null>;
|
|
66
|
+
onChange?(cb: () => void): () => void;
|
|
67
|
+
}
|
|
68
|
+
/** Prefix that marks a bearer token as an AI Matrx API key, never a JWT. */
|
|
69
|
+
declare const API_KEY_PREFIX = "mx_";
|
|
70
|
+
/**
|
|
71
|
+
* Map a credential to the exact request headers the AI Matrx platform expects.
|
|
72
|
+
* This is the single place that knowledge lives — transports spread the result
|
|
73
|
+
* into their request headers and add `X-Organization-Id` from the org context
|
|
74
|
+
* binding separately.
|
|
75
|
+
*/
|
|
76
|
+
declare function credentialToHeaders(credential: MatrxCredential | null): Record<string, string>;
|
|
77
|
+
/** What `createApi({ credentials })` accepts: a fixed credential, a getter, or a port. */
|
|
78
|
+
type CredentialsSetting = MatrxCredential | null | (() => MatrxCredential | null | Promise<MatrxCredential | null>) | CredentialsPort;
|
|
79
|
+
/** Normalize the ONE credentials setting into a `CredentialsPort`. */
|
|
80
|
+
declare function toCredentialsPort(setting: CredentialsSetting | undefined): CredentialsPort;
|
|
81
|
+
|
|
82
|
+
export { API_KEY_PREFIX, type CredentialsPort, type CredentialsSetting, MATRX_HEADERS, type MatrxCredential, type MatrxCredentialKind, credentialToHeaders, toCredentialsPort };
|