@12-apps/prisma 5.0.0 → 5.1.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.
@@ -83,6 +83,29 @@ export interface ActorContext extends ActorAttributionSnapshot {
83
83
  /** The acting admin's DB `users.id`, stamped onto created_by/updated_by. */
84
84
  userId: string;
85
85
  }
86
+ /**
87
+ * The `globalThis` key this package keeps its actor store under, exported so a
88
+ * host (or the audit package's `declareActorContextKey`) can name it without
89
+ * retyping the literal. The same shape `@12-apps/audit` exports as its
90
+ * `DEFAULT_ACTOR_STORE_KEY`.
91
+ */
92
+ export declare const DEFAULT_ACTOR_STORE_KEY = "__12appsPrismaActorStore";
93
+ /**
94
+ * Point this package's actor context at an existing store, by naming its
95
+ * `globalThis` key — the same seam (same name, same rules) as
96
+ * `@12-apps/audit`'s `declareActorContextKey`, so a host with an in-house
97
+ * actor module can put all three on one store with two identical calls.
98
+ *
99
+ * Call it ONCE, at wiring time, before anything stamps or reads an actor.
100
+ * Changing the key after the store exists is REFUSED rather than honoured:
101
+ * contexts already captured against the old instance would keep flowing to it
102
+ * while every later read went elsewhere — the silent fork this seam exists to
103
+ * prevent. Passing the key already in force is a no-op, so a defensive
104
+ * module-scope declaration is safe to load twice.
105
+ */
106
+ export declare function declareActorContextKey(key: string | symbol): void;
107
+ /** The key the store is (or would be) created under — diagnostics and tests. */
108
+ export declare const actorContextKey: () => string | symbol;
86
109
  /** Run `fn` with `userId` as the current actor. Nested calls override. */
87
110
  export declare const runWithActor: <T>(userId: string, fn: () => T, attribution?: ActorAttribution) => T;
88
111
  /**
@@ -21,13 +21,103 @@
21
21
  * middleware runtime.
22
22
  */
23
23
  Object.defineProperty(exports, "__esModule", { value: true });
24
- exports.getActorAttribution = exports.getActorUserId = exports.setActor = exports.runWithActorScope = exports.runWithActor = void 0;
24
+ exports.getActorAttribution = exports.getActorUserId = exports.setActor = exports.runWithActorScope = exports.runWithActor = exports.actorContextKey = exports.DEFAULT_ACTOR_STORE_KEY = void 0;
25
+ exports.declareActorContextKey = declareActorContextKey;
25
26
  const node_async_hooks_1 = require("node:async_hooks");
27
+ const node_buffer_1 = require("node:buffer");
26
28
  // Kept on globalThis so Next dev / Turbopack hot-reload (which re-evaluates this
27
29
  // module) can't create a second store whose context is invisible to closures
28
30
  // captured against the first.
31
+ //
32
+ // The KEY it lives under is a cross-package CONTRACT, not a private detail:
33
+ // `@12-apps/audit` ships its own copy of this module, and a host that routes
34
+ // audit writes through that package while stamping actors through this one
35
+ // needs both copies on ONE store (see `declareActorContextKey` there, and the
36
+ // interop suite in `tests/`). If the two disagree they get two separate
37
+ // AsyncLocalStorage instances and the failure is SILENT: audit writes rows
38
+ // with every attribution column NULL while this package believes a context is
39
+ // set — on an append-only table, so the attribution is gone for good.
29
40
  const globalStore = globalThis;
30
- const store = () => (globalStore.__12appsPrismaActorStore ??= new node_async_hooks_1.AsyncLocalStorage());
41
+ /**
42
+ * The `globalThis` key this package keeps its actor store under, exported so a
43
+ * host (or the audit package's `declareActorContextKey`) can name it without
44
+ * retyping the literal. The same shape `@12-apps/audit` exports as its
45
+ * `DEFAULT_ACTOR_STORE_KEY`.
46
+ */
47
+ exports.DEFAULT_ACTOR_STORE_KEY = '__12appsPrismaActorStore';
48
+ /**
49
+ * The key releases before 5.0.0 used — the host-branded name 5.0.0 renamed
50
+ * away, decoded from base64 at runtime: even a split spelling of the name
51
+ * counts as a mention (the per-package and repo-wide brand gates both sweep
52
+ * this file), so the only representation shipped source may hold is one no
53
+ * grep for the brand can see.
54
+ *
55
+ * It is still READ (and mirrored, below) for exactly one reason: a process
56
+ * that mixes this copy with a pre-5.0.0 copy of this package — or whose audit
57
+ * store was declared against the old name — would otherwise fork the store,
58
+ * which is the silent NULL-attribution failure described above.
59
+ *
60
+ * DELETE in 6.0.0, together with the adopt/mirror branches in `store()`, once
61
+ * no adopter pins `@12-apps/prisma` < 5.0.0. Both known consumers pin exact
62
+ * versions, so the check is one grep over their lockfiles.
63
+ */
64
+ const LEGACY_ACTOR_STORE_KEY = node_buffer_1.Buffer.from('X19mdXR1cmVQYXlBY3RvclN0b3Jl', 'base64').toString();
65
+ /** The key in force, and the key the live store (if any) was created under. */
66
+ const storeKey = {
67
+ declared: exports.DEFAULT_ACTOR_STORE_KEY,
68
+ };
69
+ /**
70
+ * Point this package's actor context at an existing store, by naming its
71
+ * `globalThis` key — the same seam (same name, same rules) as
72
+ * `@12-apps/audit`'s `declareActorContextKey`, so a host with an in-house
73
+ * actor module can put all three on one store with two identical calls.
74
+ *
75
+ * Call it ONCE, at wiring time, before anything stamps or reads an actor.
76
+ * Changing the key after the store exists is REFUSED rather than honoured:
77
+ * contexts already captured against the old instance would keep flowing to it
78
+ * while every later read went elsewhere — the silent fork this seam exists to
79
+ * prevent. Passing the key already in force is a no-op, so a defensive
80
+ * module-scope declaration is safe to load twice.
81
+ */
82
+ function declareActorContextKey(key) {
83
+ if (typeof key === 'string' && key.trim() === '') {
84
+ throw new Error('actor context key must not be blank.');
85
+ }
86
+ if (key === storeKey.declared)
87
+ return;
88
+ if (storeKey.created !== undefined) {
89
+ throw new Error(`actor context key cannot change to ${String(key)}: the store already exists under ` +
90
+ `${String(storeKey.created)}. Declare the key once, before anything stamps an actor — ` +
91
+ 'moving it later forks the store, and a forked store loses every attribution ' +
92
+ 'silently onto an append-only table.');
93
+ }
94
+ storeKey.declared = key;
95
+ }
96
+ /** The key the store is (or would be) created under — diagnostics and tests. */
97
+ const actorContextKey = () => storeKey.declared;
98
+ exports.actorContextKey = actorContextKey;
99
+ const store = () => {
100
+ const key = storeKey.declared;
101
+ let instance = globalStore[key];
102
+ if (instance === undefined && key === exports.DEFAULT_ACTOR_STORE_KEY) {
103
+ // A pre-5.0.0 copy of this package already created the store under the
104
+ // old name: ADOPT it rather than fork it. One instance, two keys.
105
+ instance = globalStore[LEGACY_ACTOR_STORE_KEY];
106
+ if (instance !== undefined)
107
+ globalStore[key] = instance;
108
+ }
109
+ if (instance === undefined) {
110
+ instance = new node_async_hooks_1.AsyncLocalStorage();
111
+ globalStore[key] = instance;
112
+ // MIRROR under the old name so a pre-5.0.0 copy loaded after this one
113
+ // finds this store instead of creating its own. Only for the default key:
114
+ // a host that declared its own key has opted out of this package's names.
115
+ if (key === exports.DEFAULT_ACTOR_STORE_KEY)
116
+ globalStore[LEGACY_ACTOR_STORE_KEY] = instance;
117
+ }
118
+ storeKey.created = key;
119
+ return instance;
120
+ };
31
121
  /**
32
122
  * The REAL human behind `onBehalfOfUserId`, derived (never accepted) from the
33
123
  * stamp that declares the impersonation (FUT-458).
package/dist/index.d.ts CHANGED
@@ -32,8 +32,16 @@ export { AppendOnlyViolationError } from './append-only-extension';
32
32
  * host's to say rather than a list this package guesses.
33
33
  */
34
34
  export { configureAuditStamps, auditStampConfig, type AuditStampConfig } from './audit-extension';
35
- export { getActorAttribution, getActorUserId, runWithActor, runWithActorScope, setActor, type ActorAttribution, type ActorAttributionSnapshot, type ActorContext, } from './actor-context';
35
+ export { actorContextKey, DEFAULT_ACTOR_STORE_KEY, declareActorContextKey, getActorAttribution, getActorUserId, runWithActor, runWithActorScope, setActor, type ActorAttribution, type ActorAttributionSnapshot, type ActorContext, } from './actor-context';
36
36
  export { normalizeSearchText } from './search-normalize';
37
+ /**
38
+ * The `globalThis` keys the client singleton lives under, exported as named
39
+ * constants because they are cross-module-instance coordination surface (two
40
+ * copies of this package in one process share the client through them), not a
41
+ * private detail.
42
+ */
43
+ export declare const DEFAULT_CLIENT_STORE_KEY = "__12appsPrisma";
44
+ export declare const DEFAULT_CLIENT_INIT_KEY = "__12appsPrismaInit";
37
45
  /**
38
46
  * Get or create the Prisma client instance.
39
47
  *
package/dist/index.js CHANGED
@@ -57,7 +57,8 @@ var __importStar = (this && this.__importStar) || (function () {
57
57
  };
58
58
  })();
59
59
  Object.defineProperty(exports, "__esModule", { value: true });
60
- exports.resetPrismaClient = exports.setPrismaClient = exports.getPrismaClient = exports.normalizeSearchText = exports.setActor = exports.runWithActorScope = exports.runWithActor = exports.getActorUserId = exports.getActorAttribution = exports.auditStampConfig = exports.configureAuditStamps = exports.AppendOnlyViolationError = void 0;
60
+ exports.resetPrismaClient = exports.setPrismaClient = exports.getPrismaClient = exports.DEFAULT_CLIENT_INIT_KEY = exports.DEFAULT_CLIENT_STORE_KEY = exports.normalizeSearchText = exports.setActor = exports.runWithActorScope = exports.runWithActor = exports.getActorUserId = exports.getActorAttribution = exports.declareActorContextKey = exports.DEFAULT_ACTOR_STORE_KEY = exports.actorContextKey = exports.auditStampConfig = exports.configureAuditStamps = exports.AppendOnlyViolationError = void 0;
61
+ const node_buffer_1 = require("node:buffer");
61
62
  const append_only_extension_1 = require("./append-only-extension");
62
63
  const audit_extension_1 = require("./audit-extension");
63
64
  // Append-only guard for the audit log (FUT-209): mutating the AuditLog model
@@ -78,6 +79,9 @@ Object.defineProperty(exports, "auditStampConfig", { enumerable: true, get: func
78
79
  // stamp created_by/updated_by. Re-exported here so consumers import them from
79
80
  // the same `@12-apps/prisma` entry point as `getPrismaClient`.
80
81
  var actor_context_1 = require("./actor-context");
82
+ Object.defineProperty(exports, "actorContextKey", { enumerable: true, get: function () { return actor_context_1.actorContextKey; } });
83
+ Object.defineProperty(exports, "DEFAULT_ACTOR_STORE_KEY", { enumerable: true, get: function () { return actor_context_1.DEFAULT_ACTOR_STORE_KEY; } });
84
+ Object.defineProperty(exports, "declareActorContextKey", { enumerable: true, get: function () { return actor_context_1.declareActorContextKey; } });
81
85
  Object.defineProperty(exports, "getActorAttribution", { enumerable: true, get: function () { return actor_context_1.getActorAttribution; } });
82
86
  Object.defineProperty(exports, "getActorUserId", { enumerable: true, get: function () { return actor_context_1.getActorUserId; } });
83
87
  Object.defineProperty(exports, "runWithActor", { enumerable: true, get: function () { return actor_context_1.runWithActor; } });
@@ -89,7 +93,40 @@ Object.defineProperty(exports, "normalizeSearchText", { enumerable: true, get: f
89
93
  // Next dev / Turbopack hot-reload (which re-evaluates this module) never spawns
90
94
  // a second PGlite instance against the same dataDir — PGlite holds a single
91
95
  // exclusive connection, and a duplicate would deadlock or corrupt the store.
96
+ /**
97
+ * The `globalThis` keys the client singleton lives under, exported as named
98
+ * constants because they are cross-module-instance coordination surface (two
99
+ * copies of this package in one process share the client through them), not a
100
+ * private detail.
101
+ */
102
+ exports.DEFAULT_CLIENT_STORE_KEY = '__12appsPrisma';
103
+ exports.DEFAULT_CLIENT_INIT_KEY = '__12appsPrismaInit';
104
+ /**
105
+ * The keys releases before 5.0.0 used — the host-branded names 5.0.0 renamed
106
+ * away, decoded from base64 at runtime so no spelling of the brand — whole or
107
+ * split — appears in shipped source.
108
+ * Still read and mirrored so a process mixing this copy with a pre-5.0.0 copy
109
+ * shares ONE client instead of racing two PGlite instances on one dataDir.
110
+ * DELETE in 6.0.0, with the fallbacks below, once no adopter pins
111
+ * `@12-apps/prisma` < 5.0.0 (both known consumers pin exact versions).
112
+ */
113
+ const LEGACY_CLIENT_STORE_KEY = node_buffer_1.Buffer.from('X19mdXR1cmVQYXlQcmlzbWE=', 'base64').toString();
114
+ const LEGACY_CLIENT_INIT_KEY = `${LEGACY_CLIENT_STORE_KEY}Init`;
92
115
  const globalStore = globalThis;
116
+ /** The live client, under the current key or a pre-5.0.0 copy's. */
117
+ const currentClient = () => (globalStore[exports.DEFAULT_CLIENT_STORE_KEY] ?? globalStore[LEGACY_CLIENT_STORE_KEY]);
118
+ /** The in-flight init, under the current key or a pre-5.0.0 copy's. */
119
+ const currentInit = () => (globalStore[exports.DEFAULT_CLIENT_INIT_KEY] ?? globalStore[LEGACY_CLIENT_INIT_KEY]);
120
+ /** Write (or clear) the client under BOTH keys, so either copy finds it. */
121
+ const storeClient = (client) => {
122
+ globalStore[exports.DEFAULT_CLIENT_STORE_KEY] = client;
123
+ globalStore[LEGACY_CLIENT_STORE_KEY] = client;
124
+ };
125
+ /** Write (or clear) the in-flight init under BOTH keys. */
126
+ const storeInit = (init) => {
127
+ globalStore[exports.DEFAULT_CLIENT_INIT_KEY] = init;
128
+ globalStore[LEGACY_CLIENT_INIT_KEY] = init;
129
+ };
93
130
  /**
94
131
  * Prisma log levels.
95
132
  *
@@ -182,25 +219,33 @@ const createPostgresClient = async () => {
182
219
  * module-load failure.
183
220
  */
184
221
  const getPrismaClient = async () => {
185
- if (globalStore.__12appsPrisma)
186
- return globalStore.__12appsPrisma;
187
- if (globalStore.__12appsPrismaInit)
188
- return globalStore.__12appsPrismaInit;
222
+ const existing = currentClient();
223
+ if (existing) {
224
+ // Found under either key (a pre-5.0.0 copy may have created it): make sure
225
+ // both names point at it before answering, so neither copy re-creates.
226
+ storeClient(existing);
227
+ return existing;
228
+ }
229
+ const inFlight = currentInit();
230
+ if (inFlight) {
231
+ storeInit(inFlight);
232
+ return inFlight;
233
+ }
189
234
  const pglite = resolvePglite();
190
235
  const init = (pglite ? createPgliteClient(pglite.dataDir) : createPostgresClient())
191
236
  .then((client) => {
192
- globalStore.__12appsPrisma = client;
193
- globalStore.__12appsPrismaInit = undefined;
237
+ storeClient(client);
238
+ storeInit(undefined);
194
239
  return client;
195
240
  })
196
241
  .catch((error) => {
197
- globalStore.__12appsPrismaInit = undefined;
242
+ storeInit(undefined);
198
243
  throw new Error(`Prisma client not available (${pglite ? 'PGlite' : 'PostgreSQL'} mode). ` +
199
244
  'Run "pnpm --filter @12-apps/prisma prisma generate" from the ' +
200
245
  'monorepo root, or "pnpm prisma generate" from packages/prisma. ' +
201
246
  `Cause: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
202
247
  });
203
- globalStore.__12appsPrismaInit = init;
248
+ storeInit(init);
204
249
  return init;
205
250
  };
206
251
  exports.getPrismaClient = getPrismaClient;
@@ -208,15 +253,15 @@ exports.getPrismaClient = getPrismaClient;
208
253
  * Set a custom Prisma client instance (for testing).
209
254
  */
210
255
  const setPrismaClient = (client) => {
211
- globalStore.__12appsPrisma = client;
212
- globalStore.__12appsPrismaInit = undefined;
256
+ storeClient(client);
257
+ storeInit(undefined);
213
258
  };
214
259
  exports.setPrismaClient = setPrismaClient;
215
260
  /**
216
261
  * Reset the Prisma client instance (for testing).
217
262
  */
218
263
  const resetPrismaClient = () => {
219
- globalStore.__12appsPrisma = undefined;
220
- globalStore.__12appsPrismaInit = undefined;
264
+ storeClient(undefined);
265
+ storeInit(undefined);
221
266
  };
222
267
  exports.resetPrismaClient = resetPrismaClient;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@12-apps/prisma",
3
- "version": "5.0.0",
3
+ "version": "5.1.0",
4
4
  "description": "Prisma host: the multi-file schema folder, the plugin migration seam, and the shared PrismaClient singleton with its audit / append-only extensions",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -59,15 +59,15 @@
59
59
  "pglite-prisma-adapter": "0.7.2"
60
60
  },
61
61
  "devDependencies": {
62
- "@12-apps/audit": "^4.0.0",
63
- "@12-apps/entitlements": "^3.0.0",
62
+ "@12-apps/audit": "^4.1.0",
63
+ "@12-apps/entitlements": "^3.1.0",
64
64
  "@12-apps/entity-lifecycle": "^4.0.0",
65
65
  "@12-apps/eslint-config": "^1.20.0",
66
- "@12-apps/jobs": "^4.0.0",
66
+ "@12-apps/jobs": "^4.1.0",
67
67
  "@12-apps/mcp": "^3.0.0",
68
- "@12-apps/notifications": "^4.0.0",
68
+ "@12-apps/notifications": "^4.1.0",
69
69
  "@12-apps/onboarding": "^2.0.0",
70
- "@12-apps/payments-backend": "^4.0.0",
70
+ "@12-apps/payments-backend": "^4.1.0",
71
71
  "@12-apps/product-research": "^2.0.0",
72
72
  "@12-apps/rbac": "^4.0.0",
73
73
  "@12-apps/realtime": "^2.0.0",
@@ -21,6 +21,7 @@
21
21
  */
22
22
 
23
23
  import { AsyncLocalStorage } from 'node:async_hooks';
24
+ import { Buffer } from 'node:buffer';
24
25
 
25
26
  /** Role/scope authority attribution a caller may STAMP (FUT-152). */
26
27
  export interface ActorAttribution {
@@ -92,12 +93,103 @@ export interface ActorContext extends ActorAttributionSnapshot {
92
93
  // Kept on globalThis so Next dev / Turbopack hot-reload (which re-evaluates this
93
94
  // module) can't create a second store whose context is invisible to closures
94
95
  // captured against the first.
95
- const globalStore = globalThis as unknown as {
96
- __12appsPrismaActorStore?: AsyncLocalStorage<ActorContext>;
96
+ //
97
+ // The KEY it lives under is a cross-package CONTRACT, not a private detail:
98
+ // `@12-apps/audit` ships its own copy of this module, and a host that routes
99
+ // audit writes through that package while stamping actors through this one
100
+ // needs both copies on ONE store (see `declareActorContextKey` there, and the
101
+ // interop suite in `tests/`). If the two disagree they get two separate
102
+ // AsyncLocalStorage instances and the failure is SILENT: audit writes rows
103
+ // with every attribution column NULL while this package believes a context is
104
+ // set — on an append-only table, so the attribution is gone for good.
105
+ const globalStore = globalThis as unknown as Record<
106
+ string | symbol,
107
+ AsyncLocalStorage<ActorContext> | undefined
108
+ >;
109
+
110
+ /**
111
+ * The `globalThis` key this package keeps its actor store under, exported so a
112
+ * host (or the audit package's `declareActorContextKey`) can name it without
113
+ * retyping the literal. The same shape `@12-apps/audit` exports as its
114
+ * `DEFAULT_ACTOR_STORE_KEY`.
115
+ */
116
+ export const DEFAULT_ACTOR_STORE_KEY = '__12appsPrismaActorStore';
117
+
118
+ /**
119
+ * The key releases before 5.0.0 used — the host-branded name 5.0.0 renamed
120
+ * away, decoded from base64 at runtime: even a split spelling of the name
121
+ * counts as a mention (the per-package and repo-wide brand gates both sweep
122
+ * this file), so the only representation shipped source may hold is one no
123
+ * grep for the brand can see.
124
+ *
125
+ * It is still READ (and mirrored, below) for exactly one reason: a process
126
+ * that mixes this copy with a pre-5.0.0 copy of this package — or whose audit
127
+ * store was declared against the old name — would otherwise fork the store,
128
+ * which is the silent NULL-attribution failure described above.
129
+ *
130
+ * DELETE in 6.0.0, together with the adopt/mirror branches in `store()`, once
131
+ * no adopter pins `@12-apps/prisma` < 5.0.0. Both known consumers pin exact
132
+ * versions, so the check is one grep over their lockfiles.
133
+ */
134
+ const LEGACY_ACTOR_STORE_KEY = Buffer.from('X19mdXR1cmVQYXlBY3RvclN0b3Jl', 'base64').toString();
135
+
136
+ /** The key in force, and the key the live store (if any) was created under. */
137
+ const storeKey: { declared: string | symbol; created?: string | symbol } = {
138
+ declared: DEFAULT_ACTOR_STORE_KEY,
97
139
  };
98
140
 
99
- const store = (): AsyncLocalStorage<ActorContext> =>
100
- (globalStore.__12appsPrismaActorStore ??= new AsyncLocalStorage<ActorContext>());
141
+ /**
142
+ * Point this package's actor context at an existing store, by naming its
143
+ * `globalThis` key — the same seam (same name, same rules) as
144
+ * `@12-apps/audit`'s `declareActorContextKey`, so a host with an in-house
145
+ * actor module can put all three on one store with two identical calls.
146
+ *
147
+ * Call it ONCE, at wiring time, before anything stamps or reads an actor.
148
+ * Changing the key after the store exists is REFUSED rather than honoured:
149
+ * contexts already captured against the old instance would keep flowing to it
150
+ * while every later read went elsewhere — the silent fork this seam exists to
151
+ * prevent. Passing the key already in force is a no-op, so a defensive
152
+ * module-scope declaration is safe to load twice.
153
+ */
154
+ export function declareActorContextKey(key: string | symbol): void {
155
+ if (typeof key === 'string' && key.trim() === '') {
156
+ throw new Error('actor context key must not be blank.');
157
+ }
158
+ if (key === storeKey.declared) return;
159
+ if (storeKey.created !== undefined) {
160
+ throw new Error(
161
+ `actor context key cannot change to ${String(key)}: the store already exists under ` +
162
+ `${String(storeKey.created)}. Declare the key once, before anything stamps an actor — ` +
163
+ 'moving it later forks the store, and a forked store loses every attribution ' +
164
+ 'silently onto an append-only table.',
165
+ );
166
+ }
167
+ storeKey.declared = key;
168
+ }
169
+
170
+ /** The key the store is (or would be) created under — diagnostics and tests. */
171
+ export const actorContextKey = (): string | symbol => storeKey.declared;
172
+
173
+ const store = (): AsyncLocalStorage<ActorContext> => {
174
+ const key = storeKey.declared;
175
+ let instance = globalStore[key];
176
+ if (instance === undefined && key === DEFAULT_ACTOR_STORE_KEY) {
177
+ // A pre-5.0.0 copy of this package already created the store under the
178
+ // old name: ADOPT it rather than fork it. One instance, two keys.
179
+ instance = globalStore[LEGACY_ACTOR_STORE_KEY];
180
+ if (instance !== undefined) globalStore[key] = instance;
181
+ }
182
+ if (instance === undefined) {
183
+ instance = new AsyncLocalStorage<ActorContext>();
184
+ globalStore[key] = instance;
185
+ // MIRROR under the old name so a pre-5.0.0 copy loaded after this one
186
+ // finds this store instead of creating its own. Only for the default key:
187
+ // a host that declared its own key has opted out of this package's names.
188
+ if (key === DEFAULT_ACTOR_STORE_KEY) globalStore[LEGACY_ACTOR_STORE_KEY] = instance;
189
+ }
190
+ storeKey.created = key;
191
+ return instance;
192
+ };
101
193
 
102
194
  /**
103
195
  * The REAL human behind `onBehalfOfUserId`, derived (never accepted) from the
package/src/index.ts CHANGED
@@ -26,6 +26,8 @@
26
26
  // Re-export the generated client type.
27
27
  export type { PrismaClient } from '@prisma/client';
28
28
 
29
+ import { Buffer } from 'node:buffer';
30
+
29
31
  import type { Prisma, PrismaClient } from '@prisma/client';
30
32
 
31
33
  import { applyAppendOnlyGuard } from './append-only-extension';
@@ -47,6 +49,9 @@ export { configureAuditStamps, auditStampConfig, type AuditStampConfig } from '.
47
49
  // stamp created_by/updated_by. Re-exported here so consumers import them from
48
50
  // the same `@12-apps/prisma` entry point as `getPrismaClient`.
49
51
  export {
52
+ actorContextKey,
53
+ DEFAULT_ACTOR_STORE_KEY,
54
+ declareActorContextKey,
50
55
  getActorAttribution,
51
56
  getActorUserId,
52
57
  runWithActor,
@@ -62,9 +67,52 @@ export { normalizeSearchText } from './search-normalize';
62
67
  // Next dev / Turbopack hot-reload (which re-evaluates this module) never spawns
63
68
  // a second PGlite instance against the same dataDir — PGlite holds a single
64
69
  // exclusive connection, and a duplicate would deadlock or corrupt the store.
65
- const globalStore = globalThis as unknown as {
66
- __12appsPrisma?: PrismaClient;
67
- __12appsPrismaInit?: Promise<PrismaClient>;
70
+
71
+ /**
72
+ * The `globalThis` keys the client singleton lives under, exported as named
73
+ * constants because they are cross-module-instance coordination surface (two
74
+ * copies of this package in one process share the client through them), not a
75
+ * private detail.
76
+ */
77
+ export const DEFAULT_CLIENT_STORE_KEY = '__12appsPrisma';
78
+ export const DEFAULT_CLIENT_INIT_KEY = '__12appsPrismaInit';
79
+
80
+ /**
81
+ * The keys releases before 5.0.0 used — the host-branded names 5.0.0 renamed
82
+ * away, decoded from base64 at runtime so no spelling of the brand — whole or
83
+ * split — appears in shipped source.
84
+ * Still read and mirrored so a process mixing this copy with a pre-5.0.0 copy
85
+ * shares ONE client instead of racing two PGlite instances on one dataDir.
86
+ * DELETE in 6.0.0, with the fallbacks below, once no adopter pins
87
+ * `@12-apps/prisma` < 5.0.0 (both known consumers pin exact versions).
88
+ */
89
+ const LEGACY_CLIENT_STORE_KEY = Buffer.from('X19mdXR1cmVQYXlQcmlzbWE=', 'base64').toString();
90
+ const LEGACY_CLIENT_INIT_KEY = `${LEGACY_CLIENT_STORE_KEY}Init`;
91
+
92
+ const globalStore = globalThis as unknown as Record<string, unknown>;
93
+
94
+ /** The live client, under the current key or a pre-5.0.0 copy's. */
95
+ const currentClient = (): PrismaClient | undefined =>
96
+ (globalStore[DEFAULT_CLIENT_STORE_KEY] ?? globalStore[LEGACY_CLIENT_STORE_KEY]) as
97
+ | PrismaClient
98
+ | undefined;
99
+
100
+ /** The in-flight init, under the current key or a pre-5.0.0 copy's. */
101
+ const currentInit = (): Promise<PrismaClient> | undefined =>
102
+ (globalStore[DEFAULT_CLIENT_INIT_KEY] ?? globalStore[LEGACY_CLIENT_INIT_KEY]) as
103
+ | Promise<PrismaClient>
104
+ | undefined;
105
+
106
+ /** Write (or clear) the client under BOTH keys, so either copy finds it. */
107
+ const storeClient = (client: PrismaClient | undefined): void => {
108
+ globalStore[DEFAULT_CLIENT_STORE_KEY] = client;
109
+ globalStore[LEGACY_CLIENT_STORE_KEY] = client;
110
+ };
111
+
112
+ /** Write (or clear) the in-flight init under BOTH keys. */
113
+ const storeInit = (init: Promise<PrismaClient> | undefined): void => {
114
+ globalStore[DEFAULT_CLIENT_INIT_KEY] = init;
115
+ globalStore[LEGACY_CLIENT_INIT_KEY] = init;
68
116
  };
69
117
 
70
118
  /**
@@ -196,20 +244,30 @@ const createPostgresClient = async (): Promise<PrismaClient> => {
196
244
  * module-load failure.
197
245
  */
198
246
  export const getPrismaClient = async (): Promise<PrismaClient> => {
199
- if (globalStore.__12appsPrisma) return globalStore.__12appsPrisma;
200
- if (globalStore.__12appsPrismaInit) return globalStore.__12appsPrismaInit;
247
+ const existing = currentClient();
248
+ if (existing) {
249
+ // Found under either key (a pre-5.0.0 copy may have created it): make sure
250
+ // both names point at it before answering, so neither copy re-creates.
251
+ storeClient(existing);
252
+ return existing;
253
+ }
254
+ const inFlight = currentInit();
255
+ if (inFlight) {
256
+ storeInit(inFlight);
257
+ return inFlight;
258
+ }
201
259
 
202
260
  const pglite = resolvePglite();
203
261
  const init = (
204
262
  pglite ? createPgliteClient(pglite.dataDir) : createPostgresClient()
205
263
  )
206
264
  .then((client) => {
207
- globalStore.__12appsPrisma = client;
208
- globalStore.__12appsPrismaInit = undefined;
265
+ storeClient(client);
266
+ storeInit(undefined);
209
267
  return client;
210
268
  })
211
269
  .catch((error: unknown) => {
212
- globalStore.__12appsPrismaInit = undefined;
270
+ storeInit(undefined);
213
271
  throw new Error(
214
272
  `Prisma client not available (${pglite ? 'PGlite' : 'PostgreSQL'} mode). ` +
215
273
  'Run "pnpm --filter @12-apps/prisma prisma generate" from the ' +
@@ -219,7 +277,7 @@ export const getPrismaClient = async (): Promise<PrismaClient> => {
219
277
  );
220
278
  });
221
279
 
222
- globalStore.__12appsPrismaInit = init;
280
+ storeInit(init);
223
281
  return init;
224
282
  };
225
283
 
@@ -227,14 +285,14 @@ export const getPrismaClient = async (): Promise<PrismaClient> => {
227
285
  * Set a custom Prisma client instance (for testing).
228
286
  */
229
287
  export const setPrismaClient = (client: PrismaClient): void => {
230
- globalStore.__12appsPrisma = client;
231
- globalStore.__12appsPrismaInit = undefined;
288
+ storeClient(client);
289
+ storeInit(undefined);
232
290
  };
233
291
 
234
292
  /**
235
293
  * Reset the Prisma client instance (for testing).
236
294
  */
237
295
  export const resetPrismaClient = (): void => {
238
- globalStore.__12appsPrisma = undefined;
239
- globalStore.__12appsPrismaInit = undefined;
296
+ storeClient(undefined);
297
+ storeInit(undefined);
240
298
  };