@ai-matrx/data 0.18.41 → 0.19.0

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 CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.19.0 — the one session identity
4
+
5
+ - New subpath `@ai-matrx/data/session-identity`: `createSessionIdentity({ client, words, toUserId?,
6
+ onDiagnostic? })`, `SERVER_SESSION_IDENTITY`, `authOf`, `sessionUserId` and the session types —
7
+ the acting user read from the Supabase client's session, never handed in (DD-240/DD-241). Each
8
+ product passes its own words; messaging and meet carried a copy each until now (architecture
9
+ step 11).
10
+ - New subpath `@ai-matrx/data/testing`: `fakeSession`, `withSession`, `signedInAs` for tests that
11
+ need a signed-in fake client.
12
+
13
+ Consumer action: none (additive).
14
+
15
+ ## 0.18.42
16
+
17
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
18
+ `git diff npm/data/v0.18.41..npm/data/v0.18.42 -- apps/shared/data`).
19
+ No source changes intended and no consumer action required.
20
+
3
21
  ## 0.18.41
4
22
 
5
23
  Automatic changed-only republish (docs/metadata drift since the last tag — see
@@ -0,0 +1,126 @@
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/session-identity.ts
21
+ var session_identity_exports = {};
22
+ __export(session_identity_exports, {
23
+ SERVER_SESSION_IDENTITY: () => SERVER_SESSION_IDENTITY,
24
+ authOf: () => authOf,
25
+ createSessionIdentity: () => createSessionIdentity,
26
+ noAuthSourceMessage: () => noAuthSourceMessage,
27
+ sessionUserId: () => sessionUserId,
28
+ unreadableSessionMessage: () => unreadableSessionMessage
29
+ });
30
+ module.exports = __toCommonJS(session_identity_exports);
31
+ var RESOLVING = Object.freeze({
32
+ state: "resolving",
33
+ userId: null
34
+ });
35
+ var SERVER_SESSION_IDENTITY = RESOLVING;
36
+ function authOf(client) {
37
+ if (client === null || typeof client !== "object" && typeof client !== "function") return null;
38
+ const candidate = client.auth;
39
+ if (candidate === null || typeof candidate !== "object") return null;
40
+ const auth = candidate;
41
+ if (typeof auth.getSession !== "function") return null;
42
+ if (typeof auth.onAuthStateChange !== "function") return null;
43
+ return auth;
44
+ }
45
+ function sessionUserId(session) {
46
+ const id = session?.user?.id;
47
+ return typeof id === "string" && id.length > 0 ? id : null;
48
+ }
49
+ function noAuthSourceMessage(words) {
50
+ return {
51
+ message: `${words.product} cannot tell who is signed in: the Supabase client it was given has no auth session source.`,
52
+ remedy: words.noAuthSourceRemedy
53
+ };
54
+ }
55
+ function unreadableSessionMessage(words, error) {
56
+ return `${words.product} could not read the signed-in session: ${String(
57
+ error?.message ?? error
58
+ )}`;
59
+ }
60
+ function createSessionIdentity(options) {
61
+ const { client, onDiagnostic, words } = options;
62
+ const brand = options.toUserId ?? ((id) => id);
63
+ const listeners = /* @__PURE__ */ new Set();
64
+ let current = RESOLVING;
65
+ let disposed = false;
66
+ const userIdOf = (session) => {
67
+ const id = sessionUserId(session);
68
+ return id === null ? null : brand(id);
69
+ };
70
+ const publish = (state, userId) => {
71
+ if (disposed) return;
72
+ if (current.state === state && current.userId === userId) return;
73
+ current = Object.freeze({ state, userId });
74
+ for (const listener of [...listeners]) listener();
75
+ };
76
+ const subscribeOnly = (hasAuthSource) => ({
77
+ hasAuthSource,
78
+ snapshot: () => current,
79
+ subscribe: (listener) => {
80
+ listeners.add(listener);
81
+ return () => {
82
+ listeners.delete(listener);
83
+ };
84
+ },
85
+ dispose: () => {
86
+ disposed = true;
87
+ listeners.clear();
88
+ }
89
+ });
90
+ const auth = authOf(client);
91
+ if (auth === null) {
92
+ onDiagnostic?.({ level: "error", ...noAuthSourceMessage(words) });
93
+ current = Object.freeze({ state: "resolved", userId: null });
94
+ return subscribeOnly(false);
95
+ }
96
+ void (async () => {
97
+ try {
98
+ const result = await auth.getSession();
99
+ publish("resolved", userIdOf(result?.data?.session));
100
+ } catch (error) {
101
+ onDiagnostic?.({
102
+ level: "warn",
103
+ message: unreadableSessionMessage(words, error),
104
+ remedy: words.unreadableRemedy
105
+ });
106
+ publish("resolved", null);
107
+ }
108
+ })();
109
+ const subscription = auth.onAuthStateChange((_event, session) => {
110
+ publish("resolved", userIdOf(session));
111
+ });
112
+ const base = subscribeOnly(true);
113
+ return {
114
+ hasAuthSource: true,
115
+ snapshot: base.snapshot,
116
+ subscribe: base.subscribe,
117
+ dispose: () => {
118
+ base.dispose();
119
+ try {
120
+ subscription.data.subscription.unsubscribe();
121
+ } catch {
122
+ }
123
+ }
124
+ };
125
+ }
126
+ //# sourceMappingURL=session-identity.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/session-identity.ts"],"sourcesContent":["/**\n * WHO IS ACTING — read from the Supabase client's session, never handed in (DD-240, DD-241).\n *\n * THE CLASS THIS CLOSES. A package that takes the acting user's id from its host (a `userId` prop\n * or option) gets the host's copy, written once when the app boots. Nothing rewrites that copy\n * when the domain-wide auth cookie rotates to another account, when the person signs out in\n * another tab, or when a refresh returns a different subject — so the package goes on issuing\n * reads (meet, DD-240) and writes (`sender_id`, `created_by` in messaging, DD-241) on behalf of\n * somebody the session no longer is. The defect is that a user id can be handed in at all.\n *\n * The rules:\n * - The acting user is `getSession()`'s answer, then every `onAuthStateChange` after it — the\n * same read supabase-js uses to mint the next request's JWT.\n * - `resolving` until the first read answers; nothing user-scoped is issued on a guess.\n * - `snapshot()` returns THE SAME REFERENCE until the acting user moves (a\n * `useSyncExternalStore` source that returns a fresh object per call is \"Maximum update depth\n * exceeded\", not a warning).\n * - A client with no auth source is a named misconfiguration (`hasAuthSource: false`), never a\n * silent fall back to \"nobody\".\n *\n * One home (architecture step 11): messaging and meet each carried a copy until data 0.19.0.\n * Each product passes its own words (`words`) and, when it brands ids, its `toUserId`.\n */\n\n/** What a session looks like to this module. Nothing else is read from it. */\nexport interface AuthSessionLike {\n readonly user?: { readonly id?: string | null } | null | undefined;\n}\n\nexport interface AuthSubscriptionLike {\n readonly data: { readonly subscription: { unsubscribe(): void } };\n}\n\n/** The subset of `client.auth` read here — an internal contract, never a public prop type. */\nexport interface AuthLike {\n getSession(): PromiseLike<{\n data: { session?: AuthSessionLike | null | undefined };\n }>;\n onAuthStateChange(\n callback: (event: string, session: AuthSessionLike | null) => void,\n ): AuthSubscriptionLike;\n}\n\nexport interface SessionIdentitySnapshot<Id extends string = string> {\n /** `resolving` until the first `getSession()` answers. */\n readonly state: \"resolving\" | \"resolved\";\n /** Who the session says is acting, or `null` for nobody. */\n readonly userId: Id | null;\n}\n\nexport interface SessionIdentity<Id extends string = string> {\n /** False when the client has no auth source at all — a misconfiguration, not \"signed out\". */\n readonly hasAuthSource: boolean;\n /** Stable reference until the acting user actually changes. */\n snapshot(): SessionIdentitySnapshot<Id>;\n subscribe(listener: () => void): () => void;\n dispose(): void;\n}\n\nexport interface SessionIdentityDiagnostic {\n level: \"info\" | \"warn\" | \"error\";\n message: string;\n remedy?: string | undefined;\n}\n\n/** The sentences a product says about its own session. */\nexport interface SessionIdentityWords {\n /** The product's name as the subject of a sentence, e.g. \"Messaging\". */\n readonly product: string;\n /** Remedy when the client has no auth source. */\n readonly noAuthSourceRemedy: string;\n /** Remedy when the session could not be read. */\n readonly unreadableRemedy: string;\n}\n\nexport interface SessionIdentityOptions<Id extends string = string> {\n /** The host's browser Supabase client (anything with `auth.getSession` / `onAuthStateChange`). */\n readonly client: unknown;\n readonly words: SessionIdentityWords;\n /** Brand the raw id (default: the string itself). */\n readonly toUserId?: ((id: string) => Id) | undefined;\n readonly onDiagnostic?: ((event: SessionIdentityDiagnostic) => void) | undefined;\n}\n\nconst RESOLVING: SessionIdentitySnapshot<never> = Object.freeze({\n state: \"resolving\" as const,\n userId: null,\n});\n\n/** The snapshot every server render sees: nobody is acting on a server. */\nexport const SERVER_SESSION_IDENTITY: SessionIdentitySnapshot<never> = RESOLVING;\n\n/**\n * The one narrowing. A client whose `auth` is missing or not shaped like an auth client is a real\n * failure — the caller says so and stays `resolved` with nobody acting (inert, never wrong).\n */\nexport function authOf(client: unknown): AuthLike | null {\n if (client === null || (typeof client !== \"object\" && typeof client !== \"function\")) return null;\n const candidate = (client as { auth?: unknown }).auth;\n if (candidate === null || typeof candidate !== \"object\") return null;\n const auth = candidate as Partial<AuthLike>;\n if (typeof auth.getSession !== \"function\") return null;\n if (typeof auth.onAuthStateChange !== \"function\") return null;\n return auth as AuthLike;\n}\n\n/** The session's user id, or `null` for nobody. */\nexport function sessionUserId(session: AuthSessionLike | null | undefined): string | null {\n const id = session?.user?.id;\n return typeof id === \"string\" && id.length > 0 ? id : null;\n}\n\n/** The named failure a client with no auth source gets. */\nexport function noAuthSourceMessage(words: SessionIdentityWords): { message: string; remedy: string } {\n return {\n message:\n `${words.product} cannot tell who is signed in: the Supabase client it was given has no ` +\n \"auth session source.\",\n remedy: words.noAuthSourceRemedy,\n };\n}\n\n/** The sentence for a session read that threw. */\nexport function unreadableSessionMessage(words: SessionIdentityWords, error: unknown): string {\n return `${words.product} could not read the signed-in session: ${String(\n (error as Error)?.message ?? error,\n )}`;\n}\n\nexport function createSessionIdentity<Id extends string = string>(\n options: SessionIdentityOptions<Id>,\n): SessionIdentity<Id> {\n const { client, onDiagnostic, words } = options;\n const brand = options.toUserId ?? ((id: string) => id as Id);\n const listeners = new Set<() => void>();\n let current: SessionIdentitySnapshot<Id> = RESOLVING;\n let disposed = false;\n\n const userIdOf = (session: AuthSessionLike | null | undefined): Id | null => {\n const id = sessionUserId(session);\n return id === null ? null : brand(id);\n };\n\n const publish = (state: \"resolving\" | \"resolved\", userId: Id | null): void => {\n if (disposed) return;\n // SAME REFERENCE until the fact moves.\n if (current.state === state && current.userId === userId) return;\n current = Object.freeze({ state, userId });\n for (const listener of [...listeners]) listener();\n };\n\n const subscribeOnly = (hasAuthSource: boolean): SessionIdentity<Id> => ({\n hasAuthSource,\n snapshot: () => current,\n subscribe: (listener) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n dispose: () => {\n disposed = true;\n listeners.clear();\n },\n });\n\n const auth = authOf(client);\n if (auth === null) {\n onDiagnostic?.({ level: \"error\", ...noAuthSourceMessage(words) });\n current = Object.freeze({ state: \"resolved\" as const, userId: null });\n return subscribeOnly(false);\n }\n\n // The first read. `getSession()` awaits auth-js's own initialization and its single-flighted\n // refresh, so this is the same answer the next PostgREST request's JWT will carry.\n void (async () => {\n try {\n const result = await auth.getSession();\n publish(\"resolved\", userIdOf(result?.data?.session));\n } catch (error: unknown) {\n onDiagnostic?.({\n level: \"warn\",\n message: unreadableSessionMessage(words, error),\n remedy: words.unreadableRemedy,\n });\n publish(\"resolved\", null);\n }\n })();\n\n // …and every change after it: a sign-out, an account switch, a refresh with a different subject.\n const subscription = auth.onAuthStateChange((_event, session) => {\n publish(\"resolved\", userIdOf(session));\n });\n\n const base = subscribeOnly(true);\n return {\n hasAuthSource: true,\n snapshot: base.snapshot,\n subscribe: base.subscribe,\n dispose: () => {\n base.dispose();\n try {\n subscription.data.subscription.unsubscribe();\n } catch {\n // An already-torn-down subscription is not a failure worth a sentence.\n }\n },\n };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,100 @@
1
+ /**
2
+ * WHO IS ACTING — read from the Supabase client's session, never handed in (DD-240, DD-241).
3
+ *
4
+ * THE CLASS THIS CLOSES. A package that takes the acting user's id from its host (a `userId` prop
5
+ * or option) gets the host's copy, written once when the app boots. Nothing rewrites that copy
6
+ * when the domain-wide auth cookie rotates to another account, when the person signs out in
7
+ * another tab, or when a refresh returns a different subject — so the package goes on issuing
8
+ * reads (meet, DD-240) and writes (`sender_id`, `created_by` in messaging, DD-241) on behalf of
9
+ * somebody the session no longer is. The defect is that a user id can be handed in at all.
10
+ *
11
+ * The rules:
12
+ * - The acting user is `getSession()`'s answer, then every `onAuthStateChange` after it — the
13
+ * same read supabase-js uses to mint the next request's JWT.
14
+ * - `resolving` until the first read answers; nothing user-scoped is issued on a guess.
15
+ * - `snapshot()` returns THE SAME REFERENCE until the acting user moves (a
16
+ * `useSyncExternalStore` source that returns a fresh object per call is "Maximum update depth
17
+ * exceeded", not a warning).
18
+ * - A client with no auth source is a named misconfiguration (`hasAuthSource: false`), never a
19
+ * silent fall back to "nobody".
20
+ *
21
+ * One home (architecture step 11): messaging and meet each carried a copy until data 0.19.0.
22
+ * Each product passes its own words (`words`) and, when it brands ids, its `toUserId`.
23
+ */
24
+ /** What a session looks like to this module. Nothing else is read from it. */
25
+ interface AuthSessionLike {
26
+ readonly user?: {
27
+ readonly id?: string | null;
28
+ } | null | undefined;
29
+ }
30
+ interface AuthSubscriptionLike {
31
+ readonly data: {
32
+ readonly subscription: {
33
+ unsubscribe(): void;
34
+ };
35
+ };
36
+ }
37
+ /** The subset of `client.auth` read here — an internal contract, never a public prop type. */
38
+ interface AuthLike {
39
+ getSession(): PromiseLike<{
40
+ data: {
41
+ session?: AuthSessionLike | null | undefined;
42
+ };
43
+ }>;
44
+ onAuthStateChange(callback: (event: string, session: AuthSessionLike | null) => void): AuthSubscriptionLike;
45
+ }
46
+ interface SessionIdentitySnapshot<Id extends string = string> {
47
+ /** `resolving` until the first `getSession()` answers. */
48
+ readonly state: "resolving" | "resolved";
49
+ /** Who the session says is acting, or `null` for nobody. */
50
+ readonly userId: Id | null;
51
+ }
52
+ interface SessionIdentity<Id extends string = string> {
53
+ /** False when the client has no auth source at all — a misconfiguration, not "signed out". */
54
+ readonly hasAuthSource: boolean;
55
+ /** Stable reference until the acting user actually changes. */
56
+ snapshot(): SessionIdentitySnapshot<Id>;
57
+ subscribe(listener: () => void): () => void;
58
+ dispose(): void;
59
+ }
60
+ interface SessionIdentityDiagnostic {
61
+ level: "info" | "warn" | "error";
62
+ message: string;
63
+ remedy?: string | undefined;
64
+ }
65
+ /** The sentences a product says about its own session. */
66
+ interface SessionIdentityWords {
67
+ /** The product's name as the subject of a sentence, e.g. "Messaging". */
68
+ readonly product: string;
69
+ /** Remedy when the client has no auth source. */
70
+ readonly noAuthSourceRemedy: string;
71
+ /** Remedy when the session could not be read. */
72
+ readonly unreadableRemedy: string;
73
+ }
74
+ interface SessionIdentityOptions<Id extends string = string> {
75
+ /** The host's browser Supabase client (anything with `auth.getSession` / `onAuthStateChange`). */
76
+ readonly client: unknown;
77
+ readonly words: SessionIdentityWords;
78
+ /** Brand the raw id (default: the string itself). */
79
+ readonly toUserId?: ((id: string) => Id) | undefined;
80
+ readonly onDiagnostic?: ((event: SessionIdentityDiagnostic) => void) | undefined;
81
+ }
82
+ /** The snapshot every server render sees: nobody is acting on a server. */
83
+ declare const SERVER_SESSION_IDENTITY: SessionIdentitySnapshot<never>;
84
+ /**
85
+ * The one narrowing. A client whose `auth` is missing or not shaped like an auth client is a real
86
+ * failure — the caller says so and stays `resolved` with nobody acting (inert, never wrong).
87
+ */
88
+ declare function authOf(client: unknown): AuthLike | null;
89
+ /** The session's user id, or `null` for nobody. */
90
+ declare function sessionUserId(session: AuthSessionLike | null | undefined): string | null;
91
+ /** The named failure a client with no auth source gets. */
92
+ declare function noAuthSourceMessage(words: SessionIdentityWords): {
93
+ message: string;
94
+ remedy: string;
95
+ };
96
+ /** The sentence for a session read that threw. */
97
+ declare function unreadableSessionMessage(words: SessionIdentityWords, error: unknown): string;
98
+ declare function createSessionIdentity<Id extends string = string>(options: SessionIdentityOptions<Id>): SessionIdentity<Id>;
99
+
100
+ export { type AuthLike, type AuthSessionLike, type AuthSubscriptionLike, SERVER_SESSION_IDENTITY, type SessionIdentity, type SessionIdentityDiagnostic, type SessionIdentityOptions, type SessionIdentitySnapshot, type SessionIdentityWords, authOf, createSessionIdentity, noAuthSourceMessage, sessionUserId, unreadableSessionMessage };
@@ -0,0 +1,100 @@
1
+ /**
2
+ * WHO IS ACTING — read from the Supabase client's session, never handed in (DD-240, DD-241).
3
+ *
4
+ * THE CLASS THIS CLOSES. A package that takes the acting user's id from its host (a `userId` prop
5
+ * or option) gets the host's copy, written once when the app boots. Nothing rewrites that copy
6
+ * when the domain-wide auth cookie rotates to another account, when the person signs out in
7
+ * another tab, or when a refresh returns a different subject — so the package goes on issuing
8
+ * reads (meet, DD-240) and writes (`sender_id`, `created_by` in messaging, DD-241) on behalf of
9
+ * somebody the session no longer is. The defect is that a user id can be handed in at all.
10
+ *
11
+ * The rules:
12
+ * - The acting user is `getSession()`'s answer, then every `onAuthStateChange` after it — the
13
+ * same read supabase-js uses to mint the next request's JWT.
14
+ * - `resolving` until the first read answers; nothing user-scoped is issued on a guess.
15
+ * - `snapshot()` returns THE SAME REFERENCE until the acting user moves (a
16
+ * `useSyncExternalStore` source that returns a fresh object per call is "Maximum update depth
17
+ * exceeded", not a warning).
18
+ * - A client with no auth source is a named misconfiguration (`hasAuthSource: false`), never a
19
+ * silent fall back to "nobody".
20
+ *
21
+ * One home (architecture step 11): messaging and meet each carried a copy until data 0.19.0.
22
+ * Each product passes its own words (`words`) and, when it brands ids, its `toUserId`.
23
+ */
24
+ /** What a session looks like to this module. Nothing else is read from it. */
25
+ interface AuthSessionLike {
26
+ readonly user?: {
27
+ readonly id?: string | null;
28
+ } | null | undefined;
29
+ }
30
+ interface AuthSubscriptionLike {
31
+ readonly data: {
32
+ readonly subscription: {
33
+ unsubscribe(): void;
34
+ };
35
+ };
36
+ }
37
+ /** The subset of `client.auth` read here — an internal contract, never a public prop type. */
38
+ interface AuthLike {
39
+ getSession(): PromiseLike<{
40
+ data: {
41
+ session?: AuthSessionLike | null | undefined;
42
+ };
43
+ }>;
44
+ onAuthStateChange(callback: (event: string, session: AuthSessionLike | null) => void): AuthSubscriptionLike;
45
+ }
46
+ interface SessionIdentitySnapshot<Id extends string = string> {
47
+ /** `resolving` until the first `getSession()` answers. */
48
+ readonly state: "resolving" | "resolved";
49
+ /** Who the session says is acting, or `null` for nobody. */
50
+ readonly userId: Id | null;
51
+ }
52
+ interface SessionIdentity<Id extends string = string> {
53
+ /** False when the client has no auth source at all — a misconfiguration, not "signed out". */
54
+ readonly hasAuthSource: boolean;
55
+ /** Stable reference until the acting user actually changes. */
56
+ snapshot(): SessionIdentitySnapshot<Id>;
57
+ subscribe(listener: () => void): () => void;
58
+ dispose(): void;
59
+ }
60
+ interface SessionIdentityDiagnostic {
61
+ level: "info" | "warn" | "error";
62
+ message: string;
63
+ remedy?: string | undefined;
64
+ }
65
+ /** The sentences a product says about its own session. */
66
+ interface SessionIdentityWords {
67
+ /** The product's name as the subject of a sentence, e.g. "Messaging". */
68
+ readonly product: string;
69
+ /** Remedy when the client has no auth source. */
70
+ readonly noAuthSourceRemedy: string;
71
+ /** Remedy when the session could not be read. */
72
+ readonly unreadableRemedy: string;
73
+ }
74
+ interface SessionIdentityOptions<Id extends string = string> {
75
+ /** The host's browser Supabase client (anything with `auth.getSession` / `onAuthStateChange`). */
76
+ readonly client: unknown;
77
+ readonly words: SessionIdentityWords;
78
+ /** Brand the raw id (default: the string itself). */
79
+ readonly toUserId?: ((id: string) => Id) | undefined;
80
+ readonly onDiagnostic?: ((event: SessionIdentityDiagnostic) => void) | undefined;
81
+ }
82
+ /** The snapshot every server render sees: nobody is acting on a server. */
83
+ declare const SERVER_SESSION_IDENTITY: SessionIdentitySnapshot<never>;
84
+ /**
85
+ * The one narrowing. A client whose `auth` is missing or not shaped like an auth client is a real
86
+ * failure — the caller says so and stays `resolved` with nobody acting (inert, never wrong).
87
+ */
88
+ declare function authOf(client: unknown): AuthLike | null;
89
+ /** The session's user id, or `null` for nobody. */
90
+ declare function sessionUserId(session: AuthSessionLike | null | undefined): string | null;
91
+ /** The named failure a client with no auth source gets. */
92
+ declare function noAuthSourceMessage(words: SessionIdentityWords): {
93
+ message: string;
94
+ remedy: string;
95
+ };
96
+ /** The sentence for a session read that threw. */
97
+ declare function unreadableSessionMessage(words: SessionIdentityWords, error: unknown): string;
98
+ declare function createSessionIdentity<Id extends string = string>(options: SessionIdentityOptions<Id>): SessionIdentity<Id>;
99
+
100
+ export { type AuthLike, type AuthSessionLike, type AuthSubscriptionLike, SERVER_SESSION_IDENTITY, type SessionIdentity, type SessionIdentityDiagnostic, type SessionIdentityOptions, type SessionIdentitySnapshot, type SessionIdentityWords, authOf, createSessionIdentity, noAuthSourceMessage, sessionUserId, unreadableSessionMessage };
@@ -0,0 +1,105 @@
1
+ // src/session-identity.ts
2
+ var RESOLVING = Object.freeze({
3
+ state: "resolving",
4
+ userId: null
5
+ });
6
+ var SERVER_SESSION_IDENTITY = RESOLVING;
7
+ function authOf(client) {
8
+ if (client === null || typeof client !== "object" && typeof client !== "function") return null;
9
+ const candidate = client.auth;
10
+ if (candidate === null || typeof candidate !== "object") return null;
11
+ const auth = candidate;
12
+ if (typeof auth.getSession !== "function") return null;
13
+ if (typeof auth.onAuthStateChange !== "function") return null;
14
+ return auth;
15
+ }
16
+ function sessionUserId(session) {
17
+ const id = session?.user?.id;
18
+ return typeof id === "string" && id.length > 0 ? id : null;
19
+ }
20
+ function noAuthSourceMessage(words) {
21
+ return {
22
+ message: `${words.product} cannot tell who is signed in: the Supabase client it was given has no auth session source.`,
23
+ remedy: words.noAuthSourceRemedy
24
+ };
25
+ }
26
+ function unreadableSessionMessage(words, error) {
27
+ return `${words.product} could not read the signed-in session: ${String(
28
+ error?.message ?? error
29
+ )}`;
30
+ }
31
+ function createSessionIdentity(options) {
32
+ const { client, onDiagnostic, words } = options;
33
+ const brand = options.toUserId ?? ((id) => id);
34
+ const listeners = /* @__PURE__ */ new Set();
35
+ let current = RESOLVING;
36
+ let disposed = false;
37
+ const userIdOf = (session) => {
38
+ const id = sessionUserId(session);
39
+ return id === null ? null : brand(id);
40
+ };
41
+ const publish = (state, userId) => {
42
+ if (disposed) return;
43
+ if (current.state === state && current.userId === userId) return;
44
+ current = Object.freeze({ state, userId });
45
+ for (const listener of [...listeners]) listener();
46
+ };
47
+ const subscribeOnly = (hasAuthSource) => ({
48
+ hasAuthSource,
49
+ snapshot: () => current,
50
+ subscribe: (listener) => {
51
+ listeners.add(listener);
52
+ return () => {
53
+ listeners.delete(listener);
54
+ };
55
+ },
56
+ dispose: () => {
57
+ disposed = true;
58
+ listeners.clear();
59
+ }
60
+ });
61
+ const auth = authOf(client);
62
+ if (auth === null) {
63
+ onDiagnostic?.({ level: "error", ...noAuthSourceMessage(words) });
64
+ current = Object.freeze({ state: "resolved", userId: null });
65
+ return subscribeOnly(false);
66
+ }
67
+ void (async () => {
68
+ try {
69
+ const result = await auth.getSession();
70
+ publish("resolved", userIdOf(result?.data?.session));
71
+ } catch (error) {
72
+ onDiagnostic?.({
73
+ level: "warn",
74
+ message: unreadableSessionMessage(words, error),
75
+ remedy: words.unreadableRemedy
76
+ });
77
+ publish("resolved", null);
78
+ }
79
+ })();
80
+ const subscription = auth.onAuthStateChange((_event, session) => {
81
+ publish("resolved", userIdOf(session));
82
+ });
83
+ const base = subscribeOnly(true);
84
+ return {
85
+ hasAuthSource: true,
86
+ snapshot: base.snapshot,
87
+ subscribe: base.subscribe,
88
+ dispose: () => {
89
+ base.dispose();
90
+ try {
91
+ subscription.data.subscription.unsubscribe();
92
+ } catch {
93
+ }
94
+ }
95
+ };
96
+ }
97
+ export {
98
+ SERVER_SESSION_IDENTITY,
99
+ authOf,
100
+ createSessionIdentity,
101
+ noAuthSourceMessage,
102
+ sessionUserId,
103
+ unreadableSessionMessage
104
+ };
105
+ //# sourceMappingURL=session-identity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/session-identity.ts"],"sourcesContent":["/**\n * WHO IS ACTING — read from the Supabase client's session, never handed in (DD-240, DD-241).\n *\n * THE CLASS THIS CLOSES. A package that takes the acting user's id from its host (a `userId` prop\n * or option) gets the host's copy, written once when the app boots. Nothing rewrites that copy\n * when the domain-wide auth cookie rotates to another account, when the person signs out in\n * another tab, or when a refresh returns a different subject — so the package goes on issuing\n * reads (meet, DD-240) and writes (`sender_id`, `created_by` in messaging, DD-241) on behalf of\n * somebody the session no longer is. The defect is that a user id can be handed in at all.\n *\n * The rules:\n * - The acting user is `getSession()`'s answer, then every `onAuthStateChange` after it — the\n * same read supabase-js uses to mint the next request's JWT.\n * - `resolving` until the first read answers; nothing user-scoped is issued on a guess.\n * - `snapshot()` returns THE SAME REFERENCE until the acting user moves (a\n * `useSyncExternalStore` source that returns a fresh object per call is \"Maximum update depth\n * exceeded\", not a warning).\n * - A client with no auth source is a named misconfiguration (`hasAuthSource: false`), never a\n * silent fall back to \"nobody\".\n *\n * One home (architecture step 11): messaging and meet each carried a copy until data 0.19.0.\n * Each product passes its own words (`words`) and, when it brands ids, its `toUserId`.\n */\n\n/** What a session looks like to this module. Nothing else is read from it. */\nexport interface AuthSessionLike {\n readonly user?: { readonly id?: string | null } | null | undefined;\n}\n\nexport interface AuthSubscriptionLike {\n readonly data: { readonly subscription: { unsubscribe(): void } };\n}\n\n/** The subset of `client.auth` read here — an internal contract, never a public prop type. */\nexport interface AuthLike {\n getSession(): PromiseLike<{\n data: { session?: AuthSessionLike | null | undefined };\n }>;\n onAuthStateChange(\n callback: (event: string, session: AuthSessionLike | null) => void,\n ): AuthSubscriptionLike;\n}\n\nexport interface SessionIdentitySnapshot<Id extends string = string> {\n /** `resolving` until the first `getSession()` answers. */\n readonly state: \"resolving\" | \"resolved\";\n /** Who the session says is acting, or `null` for nobody. */\n readonly userId: Id | null;\n}\n\nexport interface SessionIdentity<Id extends string = string> {\n /** False when the client has no auth source at all — a misconfiguration, not \"signed out\". */\n readonly hasAuthSource: boolean;\n /** Stable reference until the acting user actually changes. */\n snapshot(): SessionIdentitySnapshot<Id>;\n subscribe(listener: () => void): () => void;\n dispose(): void;\n}\n\nexport interface SessionIdentityDiagnostic {\n level: \"info\" | \"warn\" | \"error\";\n message: string;\n remedy?: string | undefined;\n}\n\n/** The sentences a product says about its own session. */\nexport interface SessionIdentityWords {\n /** The product's name as the subject of a sentence, e.g. \"Messaging\". */\n readonly product: string;\n /** Remedy when the client has no auth source. */\n readonly noAuthSourceRemedy: string;\n /** Remedy when the session could not be read. */\n readonly unreadableRemedy: string;\n}\n\nexport interface SessionIdentityOptions<Id extends string = string> {\n /** The host's browser Supabase client (anything with `auth.getSession` / `onAuthStateChange`). */\n readonly client: unknown;\n readonly words: SessionIdentityWords;\n /** Brand the raw id (default: the string itself). */\n readonly toUserId?: ((id: string) => Id) | undefined;\n readonly onDiagnostic?: ((event: SessionIdentityDiagnostic) => void) | undefined;\n}\n\nconst RESOLVING: SessionIdentitySnapshot<never> = Object.freeze({\n state: \"resolving\" as const,\n userId: null,\n});\n\n/** The snapshot every server render sees: nobody is acting on a server. */\nexport const SERVER_SESSION_IDENTITY: SessionIdentitySnapshot<never> = RESOLVING;\n\n/**\n * The one narrowing. A client whose `auth` is missing or not shaped like an auth client is a real\n * failure — the caller says so and stays `resolved` with nobody acting (inert, never wrong).\n */\nexport function authOf(client: unknown): AuthLike | null {\n if (client === null || (typeof client !== \"object\" && typeof client !== \"function\")) return null;\n const candidate = (client as { auth?: unknown }).auth;\n if (candidate === null || typeof candidate !== \"object\") return null;\n const auth = candidate as Partial<AuthLike>;\n if (typeof auth.getSession !== \"function\") return null;\n if (typeof auth.onAuthStateChange !== \"function\") return null;\n return auth as AuthLike;\n}\n\n/** The session's user id, or `null` for nobody. */\nexport function sessionUserId(session: AuthSessionLike | null | undefined): string | null {\n const id = session?.user?.id;\n return typeof id === \"string\" && id.length > 0 ? id : null;\n}\n\n/** The named failure a client with no auth source gets. */\nexport function noAuthSourceMessage(words: SessionIdentityWords): { message: string; remedy: string } {\n return {\n message:\n `${words.product} cannot tell who is signed in: the Supabase client it was given has no ` +\n \"auth session source.\",\n remedy: words.noAuthSourceRemedy,\n };\n}\n\n/** The sentence for a session read that threw. */\nexport function unreadableSessionMessage(words: SessionIdentityWords, error: unknown): string {\n return `${words.product} could not read the signed-in session: ${String(\n (error as Error)?.message ?? error,\n )}`;\n}\n\nexport function createSessionIdentity<Id extends string = string>(\n options: SessionIdentityOptions<Id>,\n): SessionIdentity<Id> {\n const { client, onDiagnostic, words } = options;\n const brand = options.toUserId ?? ((id: string) => id as Id);\n const listeners = new Set<() => void>();\n let current: SessionIdentitySnapshot<Id> = RESOLVING;\n let disposed = false;\n\n const userIdOf = (session: AuthSessionLike | null | undefined): Id | null => {\n const id = sessionUserId(session);\n return id === null ? null : brand(id);\n };\n\n const publish = (state: \"resolving\" | \"resolved\", userId: Id | null): void => {\n if (disposed) return;\n // SAME REFERENCE until the fact moves.\n if (current.state === state && current.userId === userId) return;\n current = Object.freeze({ state, userId });\n for (const listener of [...listeners]) listener();\n };\n\n const subscribeOnly = (hasAuthSource: boolean): SessionIdentity<Id> => ({\n hasAuthSource,\n snapshot: () => current,\n subscribe: (listener) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n dispose: () => {\n disposed = true;\n listeners.clear();\n },\n });\n\n const auth = authOf(client);\n if (auth === null) {\n onDiagnostic?.({ level: \"error\", ...noAuthSourceMessage(words) });\n current = Object.freeze({ state: \"resolved\" as const, userId: null });\n return subscribeOnly(false);\n }\n\n // The first read. `getSession()` awaits auth-js's own initialization and its single-flighted\n // refresh, so this is the same answer the next PostgREST request's JWT will carry.\n void (async () => {\n try {\n const result = await auth.getSession();\n publish(\"resolved\", userIdOf(result?.data?.session));\n } catch (error: unknown) {\n onDiagnostic?.({\n level: \"warn\",\n message: unreadableSessionMessage(words, error),\n remedy: words.unreadableRemedy,\n });\n publish(\"resolved\", null);\n }\n })();\n\n // …and every change after it: a sign-out, an account switch, a refresh with a different subject.\n const subscription = auth.onAuthStateChange((_event, session) => {\n publish(\"resolved\", userIdOf(session));\n });\n\n const base = subscribeOnly(true);\n return {\n hasAuthSource: true,\n snapshot: base.snapshot,\n subscribe: base.subscribe,\n dispose: () => {\n base.dispose();\n try {\n subscription.data.subscription.unsubscribe();\n } catch {\n // An already-torn-down subscription is not a failure worth a sentence.\n }\n },\n };\n}\n"],"mappings":";AAoFA,IAAM,YAA4C,OAAO,OAAO;AAAA,EAC9D,OAAO;AAAA,EACP,QAAQ;AACV,CAAC;AAGM,IAAM,0BAA0D;AAMhE,SAAS,OAAO,QAAkC;AACvD,MAAI,WAAW,QAAS,OAAO,WAAW,YAAY,OAAO,WAAW,WAAa,QAAO;AAC5F,QAAM,YAAa,OAA8B;AACjD,MAAI,cAAc,QAAQ,OAAO,cAAc,SAAU,QAAO;AAChE,QAAM,OAAO;AACb,MAAI,OAAO,KAAK,eAAe,WAAY,QAAO;AAClD,MAAI,OAAO,KAAK,sBAAsB,WAAY,QAAO;AACzD,SAAO;AACT;AAGO,SAAS,cAAc,SAA4D;AACxF,QAAM,KAAK,SAAS,MAAM;AAC1B,SAAO,OAAO,OAAO,YAAY,GAAG,SAAS,IAAI,KAAK;AACxD;AAGO,SAAS,oBAAoB,OAAkE;AACpG,SAAO;AAAA,IACL,SACE,GAAG,MAAM,OAAO;AAAA,IAElB,QAAQ,MAAM;AAAA,EAChB;AACF;AAGO,SAAS,yBAAyB,OAA6B,OAAwB;AAC5F,SAAO,GAAG,MAAM,OAAO,0CAA0C;AAAA,IAC9D,OAAiB,WAAW;AAAA,EAC/B,CAAC;AACH;AAEO,SAAS,sBACd,SACqB;AACrB,QAAM,EAAE,QAAQ,cAAc,MAAM,IAAI;AACxC,QAAM,QAAQ,QAAQ,aAAa,CAAC,OAAe;AACnD,QAAM,YAAY,oBAAI,IAAgB;AACtC,MAAI,UAAuC;AAC3C,MAAI,WAAW;AAEf,QAAM,WAAW,CAAC,YAA2D;AAC3E,UAAM,KAAK,cAAc,OAAO;AAChC,WAAO,OAAO,OAAO,OAAO,MAAM,EAAE;AAAA,EACtC;AAEA,QAAM,UAAU,CAAC,OAAiC,WAA4B;AAC5E,QAAI,SAAU;AAEd,QAAI,QAAQ,UAAU,SAAS,QAAQ,WAAW,OAAQ;AAC1D,cAAU,OAAO,OAAO,EAAE,OAAO,OAAO,CAAC;AACzC,eAAW,YAAY,CAAC,GAAG,SAAS,EAAG,UAAS;AAAA,EAClD;AAEA,QAAM,gBAAgB,CAAC,mBAAiD;AAAA,IACtE;AAAA,IACA,UAAU,MAAM;AAAA,IAChB,WAAW,CAAC,aAAa;AACvB,gBAAU,IAAI,QAAQ;AACtB,aAAO,MAAM;AACX,kBAAU,OAAO,QAAQ;AAAA,MAC3B;AAAA,IACF;AAAA,IACA,SAAS,MAAM;AACb,iBAAW;AACX,gBAAU,MAAM;AAAA,IAClB;AAAA,EACF;AAEA,QAAM,OAAO,OAAO,MAAM;AAC1B,MAAI,SAAS,MAAM;AACjB,mBAAe,EAAE,OAAO,SAAS,GAAG,oBAAoB,KAAK,EAAE,CAAC;AAChE,cAAU,OAAO,OAAO,EAAE,OAAO,YAAqB,QAAQ,KAAK,CAAC;AACpE,WAAO,cAAc,KAAK;AAAA,EAC5B;AAIA,QAAM,YAAY;AAChB,QAAI;AACF,YAAM,SAAS,MAAM,KAAK,WAAW;AACrC,cAAQ,YAAY,SAAS,QAAQ,MAAM,OAAO,CAAC;AAAA,IACrD,SAAS,OAAgB;AACvB,qBAAe;AAAA,QACb,OAAO;AAAA,QACP,SAAS,yBAAyB,OAAO,KAAK;AAAA,QAC9C,QAAQ,MAAM;AAAA,MAChB,CAAC;AACD,cAAQ,YAAY,IAAI;AAAA,IAC1B;AAAA,EACF,GAAG;AAGH,QAAM,eAAe,KAAK,kBAAkB,CAAC,QAAQ,YAAY;AAC/D,YAAQ,YAAY,SAAS,OAAO,CAAC;AAAA,EACvC,CAAC;AAED,QAAM,OAAO,cAAc,IAAI;AAC/B,SAAO;AAAA,IACL,eAAe;AAAA,IACf,UAAU,KAAK;AAAA,IACf,WAAW,KAAK;AAAA,IAChB,SAAS,MAAM;AACb,WAAK,QAAQ;AACb,UAAI;AACF,qBAAa,KAAK,aAAa,YAAY;AAAA,MAC7C,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
@@ -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/testing.ts
21
+ var testing_exports = {};
22
+ __export(testing_exports, {
23
+ fakeSession: () => fakeSession,
24
+ signedInAs: () => signedInAs,
25
+ withSession: () => withSession
26
+ });
27
+ module.exports = __toCommonJS(testing_exports);
28
+ function fakeSession(userId) {
29
+ let current = userId;
30
+ let reads = 0;
31
+ const listeners = /* @__PURE__ */ new Set();
32
+ const sessionOf = (id) => id === null ? null : { user: { id } };
33
+ return {
34
+ auth: {
35
+ getSession: async () => {
36
+ reads += 1;
37
+ return { data: { session: sessionOf(current) } };
38
+ },
39
+ onAuthStateChange: (callback) => {
40
+ listeners.add(callback);
41
+ return {
42
+ data: {
43
+ subscription: {
44
+ unsubscribe: () => {
45
+ listeners.delete(callback);
46
+ }
47
+ }
48
+ }
49
+ };
50
+ }
51
+ },
52
+ setUser: (next) => {
53
+ current = next;
54
+ for (const listener of [...listeners]) {
55
+ listener(next === null ? "SIGNED_OUT" : "SIGNED_IN", sessionOf(next));
56
+ }
57
+ },
58
+ reads: () => reads
59
+ };
60
+ }
61
+ function withSession(client, session) {
62
+ client.auth = session.auth;
63
+ return client;
64
+ }
65
+ function signedInAs(client, userId) {
66
+ return withSession(client, fakeSession(userId));
67
+ }
68
+ //# sourceMappingURL=testing.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/testing.ts"],"sourcesContent":["/**\n * Test helpers for packages that read the acting user from a Supabase client's session\n * (`@ai-matrx/data/session-identity`). Tests only — never imported by shipped runtime code.\n *\n * A test that wants a signed-in user says so the way the real world does: by giving the fake\n * client a session. `setUser` performs the thing that broke production — the account under the\n * tab changing while the tab stays open.\n */\nimport type { AuthSessionLike } from \"./session-identity\";\n\nexport interface FakeSession {\n readonly auth: {\n getSession(): Promise<{ data: { session: AuthSessionLike | null } }>;\n onAuthStateChange(\n callback: (event: string, session: AuthSessionLike | null) => void,\n ): { data: { subscription: { unsubscribe(): void } } };\n };\n /** Change who is signed in, exactly as a cookie rotation or a switch does. */\n setUser(userId: string | null): void;\n /** How many times the session has actually been read. */\n readonly reads: () => number;\n}\n\nexport function fakeSession(userId: string | null): FakeSession {\n let current: string | null = userId;\n let reads = 0;\n const listeners = new Set<(event: string, session: AuthSessionLike | null) => void>();\n const sessionOf = (id: string | null): AuthSessionLike | null => (id === null ? null : { user: { id } });\n return {\n auth: {\n getSession: async () => {\n reads += 1;\n return { data: { session: sessionOf(current) } };\n },\n onAuthStateChange: (callback) => {\n listeners.add(callback);\n return {\n data: {\n subscription: {\n unsubscribe: () => {\n listeners.delete(callback);\n },\n },\n },\n };\n },\n },\n setUser: (next) => {\n current = next;\n for (const listener of [...listeners]) {\n listener(next === null ? \"SIGNED_OUT\" : \"SIGNED_IN\", sessionOf(next));\n }\n },\n reads: () => reads,\n };\n}\n\n/** Attach a session to any fake client without disturbing its other methods. */\nexport function withSession<T>(client: T, session: FakeSession): T {\n (client as unknown as { auth: unknown }).auth = session.auth;\n return client;\n}\n\n/** The signed-in fake in one line, for the many tests that need nothing more. */\nexport function signedInAs<T>(client: T, userId: string): T {\n return withSession(client, fakeSession(userId));\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAuBO,SAAS,YAAY,QAAoC;AAC9D,MAAI,UAAyB;AAC7B,MAAI,QAAQ;AACZ,QAAM,YAAY,oBAAI,IAA8D;AACpF,QAAM,YAAY,CAAC,OAA+C,OAAO,OAAO,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE;AACtG,SAAO;AAAA,IACL,MAAM;AAAA,MACJ,YAAY,YAAY;AACtB,iBAAS;AACT,eAAO,EAAE,MAAM,EAAE,SAAS,UAAU,OAAO,EAAE,EAAE;AAAA,MACjD;AAAA,MACA,mBAAmB,CAAC,aAAa;AAC/B,kBAAU,IAAI,QAAQ;AACtB,eAAO;AAAA,UACL,MAAM;AAAA,YACJ,cAAc;AAAA,cACZ,aAAa,MAAM;AACjB,0BAAU,OAAO,QAAQ;AAAA,cAC3B;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,IACA,SAAS,CAAC,SAAS;AACjB,gBAAU;AACV,iBAAW,YAAY,CAAC,GAAG,SAAS,GAAG;AACrC,iBAAS,SAAS,OAAO,eAAe,aAAa,UAAU,IAAI,CAAC;AAAA,MACtE;AAAA,IACF;AAAA,IACA,OAAO,MAAM;AAAA,EACf;AACF;AAGO,SAAS,YAAe,QAAW,SAAyB;AACjE,EAAC,OAAwC,OAAO,QAAQ;AACxD,SAAO;AACT;AAGO,SAAS,WAAc,QAAW,QAAmB;AAC1D,SAAO,YAAY,QAAQ,YAAY,MAAM,CAAC;AAChD;","names":[]}
@@ -0,0 +1,38 @@
1
+ import { AuthSessionLike } from './session-identity.cjs';
2
+
3
+ /**
4
+ * Test helpers for packages that read the acting user from a Supabase client's session
5
+ * (`@ai-matrx/data/session-identity`). Tests only — never imported by shipped runtime code.
6
+ *
7
+ * A test that wants a signed-in user says so the way the real world does: by giving the fake
8
+ * client a session. `setUser` performs the thing that broke production — the account under the
9
+ * tab changing while the tab stays open.
10
+ */
11
+
12
+ interface FakeSession {
13
+ readonly auth: {
14
+ getSession(): Promise<{
15
+ data: {
16
+ session: AuthSessionLike | null;
17
+ };
18
+ }>;
19
+ onAuthStateChange(callback: (event: string, session: AuthSessionLike | null) => void): {
20
+ data: {
21
+ subscription: {
22
+ unsubscribe(): void;
23
+ };
24
+ };
25
+ };
26
+ };
27
+ /** Change who is signed in, exactly as a cookie rotation or a switch does. */
28
+ setUser(userId: string | null): void;
29
+ /** How many times the session has actually been read. */
30
+ readonly reads: () => number;
31
+ }
32
+ declare function fakeSession(userId: string | null): FakeSession;
33
+ /** Attach a session to any fake client without disturbing its other methods. */
34
+ declare function withSession<T>(client: T, session: FakeSession): T;
35
+ /** The signed-in fake in one line, for the many tests that need nothing more. */
36
+ declare function signedInAs<T>(client: T, userId: string): T;
37
+
38
+ export { type FakeSession, fakeSession, signedInAs, withSession };
@@ -0,0 +1,38 @@
1
+ import { AuthSessionLike } from './session-identity.js';
2
+
3
+ /**
4
+ * Test helpers for packages that read the acting user from a Supabase client's session
5
+ * (`@ai-matrx/data/session-identity`). Tests only — never imported by shipped runtime code.
6
+ *
7
+ * A test that wants a signed-in user says so the way the real world does: by giving the fake
8
+ * client a session. `setUser` performs the thing that broke production — the account under the
9
+ * tab changing while the tab stays open.
10
+ */
11
+
12
+ interface FakeSession {
13
+ readonly auth: {
14
+ getSession(): Promise<{
15
+ data: {
16
+ session: AuthSessionLike | null;
17
+ };
18
+ }>;
19
+ onAuthStateChange(callback: (event: string, session: AuthSessionLike | null) => void): {
20
+ data: {
21
+ subscription: {
22
+ unsubscribe(): void;
23
+ };
24
+ };
25
+ };
26
+ };
27
+ /** Change who is signed in, exactly as a cookie rotation or a switch does. */
28
+ setUser(userId: string | null): void;
29
+ /** How many times the session has actually been read. */
30
+ readonly reads: () => number;
31
+ }
32
+ declare function fakeSession(userId: string | null): FakeSession;
33
+ /** Attach a session to any fake client without disturbing its other methods. */
34
+ declare function withSession<T>(client: T, session: FakeSession): T;
35
+ /** The signed-in fake in one line, for the many tests that need nothing more. */
36
+ declare function signedInAs<T>(client: T, userId: string): T;
37
+
38
+ export { type FakeSession, fakeSession, signedInAs, withSession };
@@ -0,0 +1,47 @@
1
+ // src/testing.ts
2
+ function fakeSession(userId) {
3
+ let current = userId;
4
+ let reads = 0;
5
+ const listeners = /* @__PURE__ */ new Set();
6
+ const sessionOf = (id) => id === null ? null : { user: { id } };
7
+ return {
8
+ auth: {
9
+ getSession: async () => {
10
+ reads += 1;
11
+ return { data: { session: sessionOf(current) } };
12
+ },
13
+ onAuthStateChange: (callback) => {
14
+ listeners.add(callback);
15
+ return {
16
+ data: {
17
+ subscription: {
18
+ unsubscribe: () => {
19
+ listeners.delete(callback);
20
+ }
21
+ }
22
+ }
23
+ };
24
+ }
25
+ },
26
+ setUser: (next) => {
27
+ current = next;
28
+ for (const listener of [...listeners]) {
29
+ listener(next === null ? "SIGNED_OUT" : "SIGNED_IN", sessionOf(next));
30
+ }
31
+ },
32
+ reads: () => reads
33
+ };
34
+ }
35
+ function withSession(client, session) {
36
+ client.auth = session.auth;
37
+ return client;
38
+ }
39
+ function signedInAs(client, userId) {
40
+ return withSession(client, fakeSession(userId));
41
+ }
42
+ export {
43
+ fakeSession,
44
+ signedInAs,
45
+ withSession
46
+ };
47
+ //# sourceMappingURL=testing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/testing.ts"],"sourcesContent":["/**\n * Test helpers for packages that read the acting user from a Supabase client's session\n * (`@ai-matrx/data/session-identity`). Tests only — never imported by shipped runtime code.\n *\n * A test that wants a signed-in user says so the way the real world does: by giving the fake\n * client a session. `setUser` performs the thing that broke production — the account under the\n * tab changing while the tab stays open.\n */\nimport type { AuthSessionLike } from \"./session-identity\";\n\nexport interface FakeSession {\n readonly auth: {\n getSession(): Promise<{ data: { session: AuthSessionLike | null } }>;\n onAuthStateChange(\n callback: (event: string, session: AuthSessionLike | null) => void,\n ): { data: { subscription: { unsubscribe(): void } } };\n };\n /** Change who is signed in, exactly as a cookie rotation or a switch does. */\n setUser(userId: string | null): void;\n /** How many times the session has actually been read. */\n readonly reads: () => number;\n}\n\nexport function fakeSession(userId: string | null): FakeSession {\n let current: string | null = userId;\n let reads = 0;\n const listeners = new Set<(event: string, session: AuthSessionLike | null) => void>();\n const sessionOf = (id: string | null): AuthSessionLike | null => (id === null ? null : { user: { id } });\n return {\n auth: {\n getSession: async () => {\n reads += 1;\n return { data: { session: sessionOf(current) } };\n },\n onAuthStateChange: (callback) => {\n listeners.add(callback);\n return {\n data: {\n subscription: {\n unsubscribe: () => {\n listeners.delete(callback);\n },\n },\n },\n };\n },\n },\n setUser: (next) => {\n current = next;\n for (const listener of [...listeners]) {\n listener(next === null ? \"SIGNED_OUT\" : \"SIGNED_IN\", sessionOf(next));\n }\n },\n reads: () => reads,\n };\n}\n\n/** Attach a session to any fake client without disturbing its other methods. */\nexport function withSession<T>(client: T, session: FakeSession): T {\n (client as unknown as { auth: unknown }).auth = session.auth;\n return client;\n}\n\n/** The signed-in fake in one line, for the many tests that need nothing more. */\nexport function signedInAs<T>(client: T, userId: string): T {\n return withSession(client, fakeSession(userId));\n}\n"],"mappings":";AAuBO,SAAS,YAAY,QAAoC;AAC9D,MAAI,UAAyB;AAC7B,MAAI,QAAQ;AACZ,QAAM,YAAY,oBAAI,IAA8D;AACpF,QAAM,YAAY,CAAC,OAA+C,OAAO,OAAO,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE;AACtG,SAAO;AAAA,IACL,MAAM;AAAA,MACJ,YAAY,YAAY;AACtB,iBAAS;AACT,eAAO,EAAE,MAAM,EAAE,SAAS,UAAU,OAAO,EAAE,EAAE;AAAA,MACjD;AAAA,MACA,mBAAmB,CAAC,aAAa;AAC/B,kBAAU,IAAI,QAAQ;AACtB,eAAO;AAAA,UACL,MAAM;AAAA,YACJ,cAAc;AAAA,cACZ,aAAa,MAAM;AACjB,0BAAU,OAAO,QAAQ;AAAA,cAC3B;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,IACA,SAAS,CAAC,SAAS;AACjB,gBAAU;AACV,iBAAW,YAAY,CAAC,GAAG,SAAS,GAAG;AACrC,iBAAS,SAAS,OAAO,eAAe,aAAa,UAAU,IAAI,CAAC;AAAA,MACtE;AAAA,IACF;AAAA,IACA,OAAO,MAAM;AAAA,EACf;AACF;AAGO,SAAS,YAAe,QAAW,SAAyB;AACjE,EAAC,OAAwC,OAAO,QAAQ;AACxD,SAAO;AACT;AAGO,SAAS,WAAc,QAAW,QAAmB;AAC1D,SAAO,YAAY,QAAQ,YAAY,MAAM,CAAC;AAChD;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-matrx/data",
3
- "version": "0.18.41",
3
+ "version": "0.19.0",
4
4
  "description": "The opinionated AI Matrx data layer: the /db tier's hard-won Supabase primitives — optimistic concurrency, complete-list reads, JSONB merge, session-retry, the domain-wide auth cookie, realtime channel hygiene — the /net resilience trio, and the /files lane: the Matrx file-communication client (durable-ref resolution, the mx_files_session bootstrap, authenticated blob fetch, uploads, the public-link door).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -138,6 +138,23 @@
138
138
  "optional": true
139
139
  }
140
140
  },
141
+ "matrx": {
142
+ "charter": "The one client data and transport layer between our code, Supabase and our servers.",
143
+ "owns": [
144
+ "/db Supabase primitives",
145
+ "/net resilience",
146
+ "/files byte transport",
147
+ "session cookie",
148
+ "generated DB types (target)"
149
+ ],
150
+ "mustNotOwn": [
151
+ "feature state",
152
+ "Redux",
153
+ "UI",
154
+ "server API types (agents/generated)"
155
+ ],
156
+ "layer": 2
157
+ },
141
158
  "scripts": {
142
159
  "build": "pnpm clean && tsup && node ../scripts/check-node-env-not-inlined.mjs",
143
160
  "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",