@ionite/redis-core 0.0.24-beta.20260907.2 → 0.0.24-beta.20260907.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +2322 -12
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1,11 +1,56 @@
1
- import{accessRefId as i,accessRootId as aZ,assertRequiredSubjectType as VZ,list as BZ}from"@ionite/core";function E(Z){return typeof Z.runScript==="function"}async function m(Z,$,Q,Y){let W=Y?.ttlSeconds;if(E(Z)){let B=`local cur=redis.call('GET',KEYS[1])
1
+ // packages/redis-core/src/access-assignment-store.ts
2
+ import {
3
+ accessRefId,
4
+ accessRootId,
5
+ assertRequiredSubjectType,
6
+ list as list2
7
+ } from "@ionite/core";
8
+
9
+ // packages/redis-core/src/adapter.types.ts
10
+ function supportsRunScript(adapter) {
11
+ return typeof adapter.runScript === "function";
12
+ }
13
+
14
+ // packages/redis-core/src/atomic.ts
15
+ async function atomicCreate(adapter, key, value, opts) {
16
+ const ttl = opts?.ttlSeconds;
17
+ if (supportsRunScript(adapter)) {
18
+ const setCmd = ttl != null ? "redis.call('SET',KEYS[1],ARGV[1],'EX',ARGV[2])" : "redis.call('SET',KEYS[1],ARGV[1])";
19
+ const script = `local cur=redis.call('GET',KEYS[1])
2
20
  if cur then if cur==ARGV[1] then return 'exists_identical' else return 'conflict' end end
3
- ${W!=null?"redis.call('SET',KEYS[1],ARGV[1],'EX',ARGV[2])":"redis.call('SET',KEYS[1],ARGV[1])"}
4
- return 'created'`,U=W!=null?[Q,String(Math.max(1,Math.floor(W)))]:[Q],J=String(await Z.runScript(B,[$],U));return J==="exists_identical"||J==="conflict"?J:"created"}let V=await Z.get($);if(V!=null)return V===Q?"exists_identical":"conflict";if(W!=null)await Z.setex($,Math.max(1,Math.floor(W)),Q);else await Z.set($,Q);return"created"}async function c(Z,$,Q,Y,W){if(!E(Z))return"unsupported";let V=W!=null&&W>0?Math.max(1,Math.floor(W)):void 0,B=`local cur=redis.call('GET',KEYS[1])
21
+ ${setCmd}
22
+ return 'created'`;
23
+ const args = ttl != null ? [value, String(Math.max(1, Math.floor(ttl)))] : [value];
24
+ const res = String(await adapter.runScript(script, [key], args));
25
+ return res === "exists_identical" || res === "conflict" ? res : "created";
26
+ }
27
+ const cur = await adapter.get(key);
28
+ if (cur != null)
29
+ return cur === value ? "exists_identical" : "conflict";
30
+ if (ttl != null)
31
+ await adapter.setex(key, Math.max(1, Math.floor(ttl)), value);
32
+ else
33
+ await adapter.set(key, value);
34
+ return "created";
35
+ }
36
+ async function atomicCompareAndSet(adapter, key, expectedRaw, newRaw, newTtlSeconds) {
37
+ if (!supportsRunScript(adapter))
38
+ return "unsupported";
39
+ const ttl = newTtlSeconds != null && newTtlSeconds > 0 ? Math.max(1, Math.floor(newTtlSeconds)) : undefined;
40
+ const setCmd = ttl != null ? "redis.call('SET',KEYS[1],ARGV[2],'EX',ARGV[3])" : "redis.call('SET',KEYS[1],ARGV[2],'KEEPTTL')";
41
+ const script = `local cur=redis.call('GET',KEYS[1])
5
42
  if not cur then return 'missing' end
6
43
  if cur~=ARGV[1] then return 'retry' end
7
- ${V!=null?"redis.call('SET',KEYS[1],ARGV[2],'EX',ARGV[3])":"redis.call('SET',KEYS[1],ARGV[2],'KEEPTTL')"}
8
- return 'ok'`,U=V!=null?[Q,Y,String(V)]:[Q,Y],J=String(await Z.runScript(B,[$],U));if(J==="missing"||J==="retry")return J;return"ok"}var p=500,lZ=`local ki=1
44
+ ${setCmd}
45
+ return 'ok'`;
46
+ const args = ttl != null ? [expectedRaw, newRaw, String(ttl)] : [expectedRaw, newRaw];
47
+ const res = String(await adapter.runScript(script, [key], args));
48
+ if (res === "missing" || res === "retry")
49
+ return res;
50
+ return "ok";
51
+ }
52
+ var TXN_WRITE_CHUNK = 500;
53
+ var TXN_SCRIPT = `local ki=1
9
54
  local ai=1
10
55
  local C=tonumber(ARGV[ai]);ai=ai+1
11
56
  for i=1,C do
@@ -43,11 +88,600 @@ for i=1,W do
43
88
  redis.call('SREM',k,unpack(members))
44
89
  end
45
90
  end
46
- return 'ok'`;class D{claims=[];writes=[];claimUnique(Z,$){return this.claims.push({key:Z,owner:$}),this}set(Z,$){return this.writes.push({op:"set",key:Z,value:$}),this}setex(Z,$,Q){return this.writes.push({op:"setex",key:Z,ttl:Math.max(1,Math.floor($)),value:Q}),this}del(Z){return this.writes.push({op:"del",key:Z}),this}sadd(Z,...$){if($.length>0)this.writes.push({op:"sadd",key:Z,members:$});return this}srem(Z,...$){if($.length>0)this.writes.push({op:"srem",key:Z,members:$});return this}async exec(Z){if(E(Z))return this.execLua(Z);return this.execSequential(Z)}async execLua(Z){if(this.claims.length>0||this.writes.length<=p)return ZZ(Z,this.claims,this.writes);for(let $=0;$<this.writes.length;$+=p)await ZZ(Z,[],this.writes.slice($,$+p));return"ok"}async execSequential(Z){for(let $ of this.claims){let Q=await Z.get($.key);if(Q!=null&&Q!==$.owner)return"conflict"}for(let $ of this.writes)switch($.op){case"set":await Z.set($.key,$.value);break;case"setex":await Z.setex($.key,$.ttl,$.value);break;case"del":await Z.del($.key);break;case"sadd":await Z.sadd($.key,...$.members);break;case"srem":await Z.srem($.key,...$.members);break}return"ok"}}async function ZZ(Z,$,Q){let Y=[],W=[String($.length)];for(let G of $)Y.push(G.key),W.push(G.owner);W.push(String(Q.length));for(let G of Q)switch(G.op){case"set":W.push("set"),Y.push(G.key),W.push(G.value);break;case"setex":W.push("setex"),Y.push(G.key),W.push(String(G.ttl),G.value);break;case"del":W.push("del"),Y.push(G.key);break;case"sadd":W.push("sadd"),Y.push(G.key),W.push(String(G.members.length),...G.members);break;case"srem":W.push("srem"),Y.push(G.key),W.push(String(G.members.length),...G.members);break}return String(await Z.runScript(lZ,Y,W))==="conflict"?"conflict":"ok"}import{list as uZ,matchesListFilter as pZ}from"@ionite/core";var $Z=100,QZ=1000;function h(Z){let $=Z?.limit,Q=$==null||!Number.isFinite($)||$<=0?$Z:Math.min(Math.floor($),QZ);return{...Z,limit:Q}}function K(Z,$,Q=(Y,W)=>Y[W]){let Y=Math.max(0,$?.start??0),W=$?.limit,V=$?.filter?Z.filter((J)=>pZ({requestable:Q(J,"requestable"),userName:Q(J,"userName"),clientId:Q(J,"clientId"),externalId:Q(J,"externalId"),updatedAt:Q(J,"updatedAt")},$.filter)):Z,G=V.length,B=W!=null?Y+W:void 0,U=V.slice(Y,B);return uZ(G,U,Y,W)}import{defaultLogger as cZ}from"@ionite/core";function L(Z,$){let Q=$.log??cZ,Y={"ion.operation":"redis.revive","ion.outcome":"corrupt",store:$.store,...$.key?{key:$.key}:{}},W;try{W=JSON.parse(Z)}catch{Q.warn("Redis store: discarded corrupt JSON payload (failed to parse)",Y);return}if(W===null||typeof W!=="object"||Array.isArray(W)){Q.warn("Redis store: discarded JSON payload that is not a record object",Y);return}return W}import{createHash as dZ}from"node:crypto";import{defaultLogger as iZ}from"@ionite/core";var oZ=4096,YZ=new Set,WZ=new Set;function GZ(Z,$){Z.delete($),Z.add($);while(Z.size>oZ){let Q=Z.values().next().value;if(Q===void 0)break;Z.delete(Q)}}function _(Z,$){let Q=Z.keyPrefix;if(typeof Q!=="string"||Q.length===0)throw Error('Redis store keyPrefix is required and must be a non-empty string. Pass an explicit per-tenant prefix (e.g. "ion:t:<tenantId>:") so tenant keyspaces never collide.');if($){let Y=`${$}\x00${Q}`;if(YZ.has(Y)&&!WZ.has(Y))F(Z).warn(`Redis ${$} constructed twice with the same keyPrefix "${Q}". Each tenant MUST use a distinct keyPrefix (e.g. "ion:t:<tenantId>:") so tenant keyspaces never collide; sharing a prefix across tenants lets them read/write each other's data.`),GZ(WZ,Y);GZ(YZ,Y)}return Q}function F(Z){return Z.logger??iZ}function C(Z,...$){return Z+$.join(":")}var nZ=/^[A-Za-z0-9_.@|-]{1,64}$/;function X(Z){if(nZ.test(Z))return`=${Z}`;return`#${dZ("sha256").update(Z,"utf8").digest("base64url").slice(0,22)}`}var sZ="access:ids",rZ="access:subject:",tZ="access:target:";function eZ(Z){return JSON.stringify({...Z,createdAt:Z.createdAt.toISOString(),updatedAt:Z.updatedAt.toISOString()})}function d(Z,$){let Q=L(Z,{store:"access",log:$});if(Q===void 0)return;return{...Q,createdAt:new Date(Q.createdAt),updatedAt:new Date(Q.updatedAt)}}function UZ(Z){return{id:Z.id,subject:Z.subject,subjectType:Z.subjectType,targetType:Z.targetType,target:Z.target,resources:Z.resources,applyToApplication:Z.applyToApplication,provenance:Z.provenance,status:Z.status,metadata:Z.metadata}}function HZ(Z){let $=i(Z.subject),Q=i(Z.target);if(!$)throw Error("Access change subject must resolve to a non-empty id.");if(!Z.targetType)throw Error("Access change targetType is required.");if(!Q)throw Error("Access change target must resolve to a non-empty id.");return{id:Z.id??aZ(Z),subjectId:$,targetId:Q}}class JZ{redis;prefix;log;constructor(Z){this.redis=Z.redis,this.prefix=_(Z,"access-assignment-store"),this.log=F(Z)}key(...Z){return C(this.prefix,...Z)}idsSetKey(){return this.key(sZ)}accessStorageKey(Z){return this.key("access",X(Z))}subjectIndexKey(Z,$){return this.key(`${rZ}${$}:${X(Z)}`)}targetIndexKey(Z,$){return this.key(tZ+Z,X($))}async grant(Z){VZ(Z.subjectType,"Access change subjectType");let $=HZ(Z),Q=this.accessStorageKey($.id),Y=await this.redis.get(Q),W=Y?d(Y,this.log):void 0,V=new Date,G={...Z,id:$.id,status:Z.status??"active",subjectId:$.subjectId,targetId:$.targetId,createdAt:W?.createdAt??V,updatedAt:V},B=new D;B.set(Q,eZ(G)),B.sadd(this.idsSetKey(),$.id),B.sadd(this.subjectIndexKey($.subjectId,Z.subjectType),$.id),B.sadd(this.targetIndexKey(Z.targetType,$.targetId),$.id),await B.exec(this.redis)}async revoke(Z){let $=HZ(Z);if(await this.redis.get(this.accessStorageKey($.id))==null)return;let Y=new D;Y.del(this.accessStorageKey($.id)),Y.srem(this.idsSetKey(),$.id),Y.srem(this.subjectIndexKey($.subjectId,Z.subjectType),$.id),Y.srem(this.targetIndexKey(Z.targetType,$.targetId),$.id),await Y.exec(this.redis)}async get(Z){let $=await this.redis.get(this.accessStorageKey(Z));if($==null)return;let Q=d($,this.log);return Q?UZ(Q):void 0}async list(Z){return this.listInMemory(h(Z))}async listByTarget(Z,$,Q){let Y=await this.redis.smembers(this.targetIndexKey(Z,$.trim()));return this.listFromIds(Y,Q)}async listBySubject(Z,$,Q){VZ($);let Y=i(Z);if(!Y)return BZ(0,[],Q?.start??0,Q?.limit);let W=await this.redis.smembers(this.subjectIndexKey(Y,$));return this.listFromIds(W,Q)}async listInMemory(Z){let $=await this.redis.smembers(this.idsSetKey());return this.listFromIds($,Z)}async listFromIds(Z,$){if(Z.length===0)return BZ(0,[],$?.start??0,$?.limit);let Y=[...Z].sort().map((B)=>this.accessStorageKey(B)),V=(await this.redis.mget(Y)).filter((B)=>B!=null).map((B)=>d(B,this.log)).filter((B)=>B!=null).filter((B)=>(B.status??"active")==="active"),G=K(V,$);return{...G,items:G.items.map(UZ)}}}import{assertValidSubjectTypeScoping as MZ,deepEqualPlain as Z$,readyCatalogEntry as OZ}from"@ionite/core";class zZ{groups;roles;permissions;listeners=new Set;constructor(Z){let $=Z.redis,Q=_(Z,"access-catalog-store"),Y=F(Z),W=(V)=>{for(let G of this.listeners)G(V)};this.groups=new I($,Q,"group",W,Y),this.roles=new I($,Q,"role",W,Y),this.permissions=new I($,Q,"permission",W,Y)}afterChange(Z){return this.listeners.add(Z),()=>{this.listeners.delete(Z)}}}class XZ{redis;prefix;log;listeners=new Set;constructor(Z){this.redis=Z.redis,this.prefix=_(Z,"resource-store"),this.log=F(Z)}key(...Z){return C(this.prefix,"authz","resource",...Z)}entityKey(Z){return this.key(X(Z))}idsSetKey(){return this.key("ids")}async get(Z){let $=await this.redis.get(this.entityKey(Z));if($==null)return;let Q=L($,{store:"authz:resource",key:Z,log:this.log});return Q===void 0?void 0:Q}async list(Z){let $=await this.redis.smembers(this.idsSetKey());if($.length===0)return K([],Z);let Y=(await this.redis.mget($.map((W)=>this.entityKey(W)))).filter((W)=>W!=null).map((W)=>L(W,{store:"authz:resource",log:this.log})).filter((W)=>W!==void 0).map((W)=>W);return K(Y,Z)}async children(Z){return(await this.list()).items.filter((Q)=>{return(typeof Q.parent==="string"?Q.parent:Q.parent?.id)===Z})}changed(Z){return this.listeners.add(Z),()=>{this.listeners.delete(Z)}}async set(Z){await new D().set(this.entityKey(Z.id),JSON.stringify(Z)).sadd(this.idsSetKey(),Z.id).exec(this.redis),this.emitChange({id:Z.id})}async delete(Z){await new D().del(this.entityKey(Z)).srem(this.idsSetKey(),Z).exec(this.redis),this.emitChange({id:Z,deleted:!0})}emitChange(Z){for(let $ of this.listeners)$(Z)}}class I{redis;prefix;kind;emit;log;constructor(Z,$,Q,Y,W){this.redis=Z;this.prefix=$;this.kind=Q;this.emit=Y;this.log=W}key(...Z){return C(this.prefix,"authz",this.kind,...Z)}idsSetKey(){return this.key("ids")}entityKey(Z){return this.key(X(Z))}async get(Z){let $=await this.redis.get(this.entityKey(Z));if($==null)return;let Q=L($,{store:`authz:${this.kind}`,key:Z,log:this.log});return Q===void 0?void 0:Q}set(Z){MZ(Z);let $,Q=!0,Y=(async()=>{$=await this.get(Z.id),Q=!$||!Z$($,Z);let W=new D;W.set(this.entityKey(Z.id),JSON.stringify(Z)),W.sadd(this.idsSetKey(),Z.id),await W.exec(this.redis)})();return Y.then(()=>{if(!Q)return;this.emit({op:"set",kind:this.kind,entity:Z,previous:$})},(W)=>{this.log?.warn("authz catalog store: background persist failed",{"ion.operation":`authz.catalog.${this.kind}.set`,"ion.outcome":"error",id:Z.id,error:W instanceof Error?W.message:String(W)})}),OZ(Z,Y)}insertIfAbsent(Z){MZ(Z);let $={...Z},Q=!1,Y=(async()=>{if(!E(this.redis))throw Error("RedisAccessCatalogStore.insertIfAbsent requires a Redis adapter with runScript support.");let W=JSON.stringify(Z),V=await this.redis.runScript(`local cur=redis.call('GET',KEYS[1])
91
+ return 'ok'`;
92
+
93
+ class RedisTxn {
94
+ claims = [];
95
+ writes = [];
96
+ claimUnique(indexKey, ownerId) {
97
+ this.claims.push({ key: indexKey, owner: ownerId });
98
+ return this;
99
+ }
100
+ set(key, value) {
101
+ this.writes.push({ op: "set", key, value });
102
+ return this;
103
+ }
104
+ setex(key, ttl, value) {
105
+ this.writes.push({ op: "setex", key, ttl: Math.max(1, Math.floor(ttl)), value });
106
+ return this;
107
+ }
108
+ del(key) {
109
+ this.writes.push({ op: "del", key });
110
+ return this;
111
+ }
112
+ sadd(key, ...members) {
113
+ if (members.length > 0)
114
+ this.writes.push({ op: "sadd", key, members });
115
+ return this;
116
+ }
117
+ srem(key, ...members) {
118
+ if (members.length > 0)
119
+ this.writes.push({ op: "srem", key, members });
120
+ return this;
121
+ }
122
+ async exec(adapter) {
123
+ if (supportsRunScript(adapter))
124
+ return this.execLua(adapter);
125
+ return this.execSequential(adapter);
126
+ }
127
+ async execLua(adapter) {
128
+ if (this.claims.length > 0 || this.writes.length <= TXN_WRITE_CHUNK) {
129
+ return runTxnScript(adapter, this.claims, this.writes);
130
+ }
131
+ for (let i = 0;i < this.writes.length; i += TXN_WRITE_CHUNK) {
132
+ await runTxnScript(adapter, [], this.writes.slice(i, i + TXN_WRITE_CHUNK));
133
+ }
134
+ return "ok";
135
+ }
136
+ async execSequential(adapter) {
137
+ for (const claim of this.claims) {
138
+ const v = await adapter.get(claim.key);
139
+ if (v != null && v !== claim.owner)
140
+ return "conflict";
141
+ }
142
+ for (const w of this.writes) {
143
+ switch (w.op) {
144
+ case "set":
145
+ await adapter.set(w.key, w.value);
146
+ break;
147
+ case "setex":
148
+ await adapter.setex(w.key, w.ttl, w.value);
149
+ break;
150
+ case "del":
151
+ await adapter.del(w.key);
152
+ break;
153
+ case "sadd":
154
+ await adapter.sadd(w.key, ...w.members);
155
+ break;
156
+ case "srem":
157
+ await adapter.srem(w.key, ...w.members);
158
+ break;
159
+ }
160
+ }
161
+ return "ok";
162
+ }
163
+ }
164
+ async function runTxnScript(adapter, claims, writes) {
165
+ const keys = [];
166
+ const args = [String(claims.length)];
167
+ for (const claim of claims) {
168
+ keys.push(claim.key);
169
+ args.push(claim.owner);
170
+ }
171
+ args.push(String(writes.length));
172
+ for (const w of writes) {
173
+ switch (w.op) {
174
+ case "set":
175
+ args.push("set");
176
+ keys.push(w.key);
177
+ args.push(w.value);
178
+ break;
179
+ case "setex":
180
+ args.push("setex");
181
+ keys.push(w.key);
182
+ args.push(String(w.ttl), w.value);
183
+ break;
184
+ case "del":
185
+ args.push("del");
186
+ keys.push(w.key);
187
+ break;
188
+ case "sadd":
189
+ args.push("sadd");
190
+ keys.push(w.key);
191
+ args.push(String(w.members.length), ...w.members);
192
+ break;
193
+ case "srem":
194
+ args.push("srem");
195
+ keys.push(w.key);
196
+ args.push(String(w.members.length), ...w.members);
197
+ break;
198
+ }
199
+ }
200
+ const res = String(await adapter.runScript(TXN_SCRIPT, keys, args));
201
+ return res === "conflict" ? "conflict" : "ok";
202
+ }
203
+
204
+ // packages/redis-core/src/list-helpers.ts
205
+ import { list, matchesListFilter } from "@ionite/core";
206
+ var DEFAULT_LIST_LIMIT = 100;
207
+ var MAX_LIST_LIMIT = 1000;
208
+ function normalizeListLimit(options) {
209
+ const requested = options?.limit;
210
+ const limit = requested == null || !Number.isFinite(requested) || requested <= 0 ? DEFAULT_LIST_LIMIT : Math.min(Math.floor(requested), MAX_LIST_LIMIT);
211
+ return { ...options, limit };
212
+ }
213
+ function applyListOptions(arr, options, getField = (item, field) => item[field]) {
214
+ const start = Math.max(0, options?.start ?? 0);
215
+ const limit = options?.limit;
216
+ const filtered = options?.filter ? arr.filter((item) => matchesListFilter({
217
+ requestable: getField(item, "requestable"),
218
+ userName: getField(item, "userName"),
219
+ clientId: getField(item, "clientId"),
220
+ externalId: getField(item, "externalId"),
221
+ updatedAt: getField(item, "updatedAt")
222
+ }, options.filter)) : arr;
223
+ const total = filtered.length;
224
+ const sliceEnd = limit != null ? start + limit : undefined;
225
+ const items = filtered.slice(start, sliceEnd);
226
+ return list(total, items, start, limit);
227
+ }
228
+
229
+ // packages/redis-core/src/parse-helpers.ts
230
+ import { defaultLogger } from "@ionite/core";
231
+ function safeJsonParse(data, context) {
232
+ const log = context.log ?? defaultLogger;
233
+ const meta = {
234
+ "ion.operation": "redis.revive",
235
+ "ion.outcome": "corrupt",
236
+ store: context.store,
237
+ ...context.key ? { key: context.key } : {}
238
+ };
239
+ let parsed;
240
+ try {
241
+ parsed = JSON.parse(data);
242
+ } catch {
243
+ log.warn("Redis store: discarded corrupt JSON payload (failed to parse)", meta);
244
+ return;
245
+ }
246
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
247
+ log.warn("Redis store: discarded JSON payload that is not a record object", meta);
248
+ return;
249
+ }
250
+ return parsed;
251
+ }
252
+
253
+ // packages/redis-core/src/redis-options.ts
254
+ import { createHash } from "node:crypto";
255
+ import { defaultLogger as defaultLogger2 } from "@ionite/core";
256
+ var MAX_KEY_PREFIX_FINGERPRINTS = 4096;
257
+ var constructedKeyPrefixes = new Set;
258
+ var warnedKeyPrefixes = new Set;
259
+ function rememberBounded(set, fingerprint) {
260
+ set.delete(fingerprint);
261
+ set.add(fingerprint);
262
+ while (set.size > MAX_KEY_PREFIX_FINGERPRINTS) {
263
+ const oldest = set.values().next().value;
264
+ if (oldest === undefined)
265
+ break;
266
+ set.delete(oldest);
267
+ }
268
+ }
269
+ function getKeyPrefix(options, storeKind) {
270
+ const keyPrefix = options.keyPrefix;
271
+ if (typeof keyPrefix !== "string" || keyPrefix.length === 0) {
272
+ throw new Error('Redis store keyPrefix is required and must be a non-empty string. Pass an explicit per-tenant prefix (e.g. "ion:t:<tenantId>:") so tenant keyspaces never collide.');
273
+ }
274
+ if (storeKind) {
275
+ const fingerprint = `${storeKind}\x00${keyPrefix}`;
276
+ if (constructedKeyPrefixes.has(fingerprint) && !warnedKeyPrefixes.has(fingerprint)) {
277
+ getLogger(options).warn(`Redis ${storeKind} constructed twice with the same keyPrefix "${keyPrefix}". Each tenant MUST use a distinct keyPrefix (e.g. "ion:t:<tenantId>:") so tenant keyspaces never collide; sharing a prefix across tenants lets them read/write each other's data.`);
278
+ rememberBounded(warnedKeyPrefixes, fingerprint);
279
+ }
280
+ rememberBounded(constructedKeyPrefixes, fingerprint);
281
+ }
282
+ return keyPrefix;
283
+ }
284
+ function getLogger(options) {
285
+ return options.logger ?? defaultLogger2;
286
+ }
287
+ function prefixedKey(prefix, ...parts) {
288
+ return prefix + parts.join(":");
289
+ }
290
+ var SAFE_KEY_SEGMENT = /^[A-Za-z0-9_.@|-]{1,64}$/;
291
+ function encodeKeyId(id) {
292
+ if (SAFE_KEY_SEGMENT.test(id))
293
+ return `=${id}`;
294
+ return `#${createHash("sha256").update(id, "utf8").digest("base64url").slice(0, 22)}`;
295
+ }
296
+
297
+ // packages/redis-core/src/access-assignment-store.ts
298
+ var IDS_SET = "access:ids";
299
+ var SUBJECT_INDEX_PREFIX = "access:subject:";
300
+ var TARGET_INDEX_PREFIX = "access:target:";
301
+ function serializeAccess(change) {
302
+ return JSON.stringify({
303
+ ...change,
304
+ createdAt: change.createdAt.toISOString(),
305
+ updatedAt: change.updatedAt.toISOString()
306
+ });
307
+ }
308
+ function reviveAccess(data, log) {
309
+ const raw = safeJsonParse(data, { store: "access", log });
310
+ if (raw === undefined)
311
+ return;
312
+ return {
313
+ ...raw,
314
+ createdAt: new Date(raw.createdAt),
315
+ updatedAt: new Date(raw.updatedAt)
316
+ };
317
+ }
318
+ function toAccessChange(stored) {
319
+ return {
320
+ id: stored.id,
321
+ subject: stored.subject,
322
+ subjectType: stored.subjectType,
323
+ targetType: stored.targetType,
324
+ target: stored.target,
325
+ resources: stored.resources,
326
+ applyToApplication: stored.applyToApplication,
327
+ provenance: stored.provenance,
328
+ status: stored.status,
329
+ metadata: stored.metadata
330
+ };
331
+ }
332
+ function resolveAccess(change) {
333
+ const subjectId = accessRefId(change.subject);
334
+ const targetId = accessRefId(change.target);
335
+ if (!subjectId) {
336
+ throw new Error("Access change subject must resolve to a non-empty id.");
337
+ }
338
+ if (!change.targetType) {
339
+ throw new Error("Access change targetType is required.");
340
+ }
341
+ if (!targetId) {
342
+ throw new Error("Access change target must resolve to a non-empty id.");
343
+ }
344
+ return {
345
+ id: change.id ?? accessRootId(change),
346
+ subjectId,
347
+ targetId
348
+ };
349
+ }
350
+
351
+ class RedisAccessAssignmentStore {
352
+ redis;
353
+ prefix;
354
+ log;
355
+ constructor(options) {
356
+ this.redis = options.redis;
357
+ this.prefix = getKeyPrefix(options, "access-assignment-store");
358
+ this.log = getLogger(options);
359
+ }
360
+ key(...parts) {
361
+ return prefixedKey(this.prefix, ...parts);
362
+ }
363
+ idsSetKey() {
364
+ return this.key(IDS_SET);
365
+ }
366
+ accessStorageKey(id) {
367
+ return this.key("access", encodeKeyId(id));
368
+ }
369
+ subjectIndexKey(subjectId, subjectType) {
370
+ return this.key(`${SUBJECT_INDEX_PREFIX}${subjectType}:${encodeKeyId(subjectId)}`);
371
+ }
372
+ targetIndexKey(targetType, targetId) {
373
+ return this.key(TARGET_INDEX_PREFIX + targetType, encodeKeyId(targetId));
374
+ }
375
+ async grant(change) {
376
+ assertRequiredSubjectType(change.subjectType, "Access change subjectType");
377
+ const resolved = resolveAccess(change);
378
+ const key = this.accessStorageKey(resolved.id);
379
+ const existingRaw = await this.redis.get(key);
380
+ const existing = existingRaw ? reviveAccess(existingRaw, this.log) : undefined;
381
+ const now = new Date;
382
+ const stored = {
383
+ ...change,
384
+ id: resolved.id,
385
+ status: change.status ?? "active",
386
+ subjectId: resolved.subjectId,
387
+ targetId: resolved.targetId,
388
+ createdAt: existing?.createdAt ?? now,
389
+ updatedAt: now
390
+ };
391
+ const txn = new RedisTxn;
392
+ txn.set(key, serializeAccess(stored));
393
+ txn.sadd(this.idsSetKey(), resolved.id);
394
+ txn.sadd(this.subjectIndexKey(resolved.subjectId, change.subjectType), resolved.id);
395
+ txn.sadd(this.targetIndexKey(change.targetType, resolved.targetId), resolved.id);
396
+ await txn.exec(this.redis);
397
+ }
398
+ async revoke(change) {
399
+ const resolved = resolveAccess(change);
400
+ const existingRaw = await this.redis.get(this.accessStorageKey(resolved.id));
401
+ if (existingRaw == null)
402
+ return;
403
+ const txn = new RedisTxn;
404
+ txn.del(this.accessStorageKey(resolved.id));
405
+ txn.srem(this.idsSetKey(), resolved.id);
406
+ txn.srem(this.subjectIndexKey(resolved.subjectId, change.subjectType), resolved.id);
407
+ txn.srem(this.targetIndexKey(change.targetType, resolved.targetId), resolved.id);
408
+ await txn.exec(this.redis);
409
+ }
410
+ async get(id) {
411
+ const raw = await this.redis.get(this.accessStorageKey(id));
412
+ if (raw == null)
413
+ return;
414
+ const stored = reviveAccess(raw, this.log);
415
+ return stored ? toAccessChange(stored) : undefined;
416
+ }
417
+ async list(options) {
418
+ return this.listInMemory(normalizeListLimit(options));
419
+ }
420
+ async listByTarget(targetType, target, options) {
421
+ const ids = await this.redis.smembers(this.targetIndexKey(targetType, target.trim()));
422
+ return this.listFromIds(ids, options);
423
+ }
424
+ async listBySubject(subject, subjectType, options) {
425
+ assertRequiredSubjectType(subjectType);
426
+ const subjectId = accessRefId(subject);
427
+ if (!subjectId)
428
+ return list2(0, [], options?.start ?? 0, options?.limit);
429
+ const ids = await this.redis.smembers(this.subjectIndexKey(subjectId, subjectType));
430
+ return this.listFromIds(ids, options);
431
+ }
432
+ async listInMemory(options) {
433
+ const ids = await this.redis.smembers(this.idsSetKey());
434
+ return this.listFromIds(ids, options);
435
+ }
436
+ async listFromIds(ids, options) {
437
+ if (ids.length === 0)
438
+ return list2(0, [], options?.start ?? 0, options?.limit);
439
+ const sortedIds = [...ids].sort();
440
+ const keys = sortedIds.map((id) => this.accessStorageKey(id));
441
+ const values = await this.redis.mget(keys);
442
+ const arr = values.filter((v) => v != null).map((v) => reviveAccess(v, this.log)).filter((stored) => stored != null).filter((stored) => (stored.status ?? "active") === "active");
443
+ const result = applyListOptions(arr, options);
444
+ return {
445
+ ...result,
446
+ items: result.items.map(toAccessChange)
447
+ };
448
+ }
449
+ }
450
+ // packages/redis-core/src/access-catalog-store.ts
451
+ import {
452
+ assertValidSubjectTypeScoping,
453
+ deepEqualPlain,
454
+ readyCatalogEntry
455
+ } from "@ionite/core";
456
+ class RedisAccessCatalogStore {
457
+ groups;
458
+ roles;
459
+ permissions;
460
+ listeners = new Set;
461
+ constructor(options) {
462
+ const redis = options.redis;
463
+ const prefix = getKeyPrefix(options, "access-catalog-store");
464
+ const log = getLogger(options);
465
+ const emit = (change) => {
466
+ for (const listener of this.listeners)
467
+ listener(change);
468
+ };
469
+ this.groups = new RedisCatalogEntityStore(redis, prefix, "group", emit, log);
470
+ this.roles = new RedisCatalogEntityStore(redis, prefix, "role", emit, log);
471
+ this.permissions = new RedisCatalogEntityStore(redis, prefix, "permission", emit, log);
472
+ }
473
+ afterChange(listener) {
474
+ this.listeners.add(listener);
475
+ return () => {
476
+ this.listeners.delete(listener);
477
+ };
478
+ }
479
+ }
480
+
481
+ class RedisResourceStore {
482
+ redis;
483
+ prefix;
484
+ log;
485
+ listeners = new Set;
486
+ constructor(options) {
487
+ this.redis = options.redis;
488
+ this.prefix = getKeyPrefix(options, "resource-store");
489
+ this.log = getLogger(options);
490
+ }
491
+ key(...parts) {
492
+ return prefixedKey(this.prefix, "authz", "resource", ...parts);
493
+ }
494
+ entityKey(id) {
495
+ return this.key(encodeKeyId(id));
496
+ }
497
+ idsSetKey() {
498
+ return this.key("ids");
499
+ }
500
+ async get(id) {
501
+ const raw = await this.redis.get(this.entityKey(id));
502
+ if (raw == null)
503
+ return;
504
+ const parsed = safeJsonParse(raw, { store: "authz:resource", key: id, log: this.log });
505
+ return parsed === undefined ? undefined : parsed;
506
+ }
507
+ async list(options) {
508
+ const ids = await this.redis.smembers(this.idsSetKey());
509
+ if (ids.length === 0)
510
+ return applyListOptions([], options);
511
+ const values = await this.redis.mget(ids.map((id) => this.entityKey(id)));
512
+ const items = values.filter((value) => value != null).map((value) => safeJsonParse(value, { store: "authz:resource", log: this.log })).filter((parsed) => parsed !== undefined).map((parsed) => parsed);
513
+ return applyListOptions(items, options);
514
+ }
515
+ async children(id) {
516
+ const all = await this.list();
517
+ return all.items.filter((resource) => {
518
+ const parent = typeof resource.parent === "string" ? resource.parent : resource.parent?.id;
519
+ return parent === id;
520
+ });
521
+ }
522
+ changed(listener) {
523
+ this.listeners.add(listener);
524
+ return () => {
525
+ this.listeners.delete(listener);
526
+ };
527
+ }
528
+ async set(resource) {
529
+ await new RedisTxn().set(this.entityKey(resource.id), JSON.stringify(resource)).sadd(this.idsSetKey(), resource.id).exec(this.redis);
530
+ this.emitChange({ id: resource.id });
531
+ }
532
+ async delete(id) {
533
+ await new RedisTxn().del(this.entityKey(id)).srem(this.idsSetKey(), id).exec(this.redis);
534
+ this.emitChange({ id, deleted: true });
535
+ }
536
+ emitChange(change) {
537
+ for (const listener of this.listeners)
538
+ listener(change);
539
+ }
540
+ }
541
+
542
+ class RedisCatalogEntityStore {
543
+ redis;
544
+ prefix;
545
+ kind;
546
+ emit;
547
+ log;
548
+ constructor(redis, prefix, kind, emit, log) {
549
+ this.redis = redis;
550
+ this.prefix = prefix;
551
+ this.kind = kind;
552
+ this.emit = emit;
553
+ this.log = log;
554
+ }
555
+ key(...parts) {
556
+ return prefixedKey(this.prefix, "authz", this.kind, ...parts);
557
+ }
558
+ idsSetKey() {
559
+ return this.key("ids");
560
+ }
561
+ entityKey(id) {
562
+ return this.key(encodeKeyId(id));
563
+ }
564
+ async get(id) {
565
+ const raw = await this.redis.get(this.entityKey(id));
566
+ if (raw == null)
567
+ return;
568
+ const parsed = safeJsonParse(raw, { store: `authz:${this.kind}`, key: id, log: this.log });
569
+ return parsed === undefined ? undefined : parsed;
570
+ }
571
+ set(entity) {
572
+ assertValidSubjectTypeScoping(entity);
573
+ let previous;
574
+ let changed = true;
575
+ const write = (async () => {
576
+ previous = await this.get(entity.id);
577
+ changed = !previous || !deepEqualPlain(previous, entity);
578
+ const txn = new RedisTxn;
579
+ txn.set(this.entityKey(entity.id), JSON.stringify(entity));
580
+ txn.sadd(this.idsSetKey(), entity.id);
581
+ await txn.exec(this.redis);
582
+ })();
583
+ write.then(() => {
584
+ if (!changed)
585
+ return;
586
+ this.emit({
587
+ op: "set",
588
+ kind: this.kind,
589
+ entity,
590
+ previous
591
+ });
592
+ }, (error) => {
593
+ this.log?.warn("authz catalog store: background persist failed", {
594
+ "ion.operation": `authz.catalog.${this.kind}.set`,
595
+ "ion.outcome": "error",
596
+ id: entity.id,
597
+ error: error instanceof Error ? error.message : String(error)
598
+ });
599
+ });
600
+ return readyCatalogEntry(entity, write);
601
+ }
602
+ insertIfAbsent(entity) {
603
+ assertValidSubjectTypeScoping(entity);
604
+ const winner = { ...entity };
605
+ let created = false;
606
+ const write = (async () => {
607
+ if (!supportsRunScript(this.redis)) {
608
+ throw new Error("RedisAccessCatalogStore.insertIfAbsent requires a Redis adapter with runScript support.");
609
+ }
610
+ const raw = JSON.stringify(entity);
611
+ const result = await this.redis.runScript(`local cur=redis.call('GET',KEYS[1])
47
612
  if cur then return {0,cur} end
48
613
  redis.call('SET',KEYS[1],ARGV[1])
49
614
  redis.call('SADD',KEYS[2],ARGV[2])
50
- return {1,ARGV[1]}`,[this.entityKey(Z.id),this.idsSetKey()],[W,Z.id]);if(!Array.isArray(V)||V.length<2)throw Error("RedisAccessCatalogStore.insertIfAbsent received an invalid Redis response.");Q=String(V[0])==="1";let G=String(V[1]),B=L(G,{store:`authz:${this.kind}`,key:Z.id,log:this.log});if(!B)throw Error("RedisAccessCatalogStore.insertIfAbsent could not parse the winning row.");let U=$;for(let J of Object.keys(U))delete U[J];Object.assign(U,B)})();return Y.then(()=>{if(!Q)return;this.emit({op:"set",kind:this.kind,entity:$,previous:void 0})},(W)=>{this.log?.warn("authz catalog store: atomic insert-if-absent failed",{"ion.operation":`authz.catalog.${this.kind}.insert_if_absent`,"ion.outcome":"error",id:Z.id,error:W instanceof Error?W.message:String(W)})}),OZ($,Y)}async delete(Z){let $=await this.get(Z),Q=new D;if(Q.del(this.entityKey(Z)),Q.srem(this.idsSetKey(),Z),await Q.exec(this.redis),!$)return;this.emit({op:"delete",kind:this.kind,entity:$})}async list(Z){let $=await this.redis.smembers(this.idsSetKey());if($.length===0)return K([],Z);let Y=(await this.redis.mget($.map((W)=>this.entityKey(W)))).filter((W)=>W!=null).map((W)=>L(W,{store:`authz:${this.kind}`,log:this.log})).filter((W)=>W!==void 0).map((W)=>W);return K(Y,Z)}}import{accessRefId as R,assertRequiredSubjectType as w}from"@ionite/core";var y="__app__",qZ=100;function $$(Z){if(!Array.isArray(Z))return[];return Z.map(($)=>$==null?"":String($)).filter(($)=>$.length>0)}var Q$=`local rootId=ARGV[1]
615
+ return {1,ARGV[1]}`, [this.entityKey(entity.id), this.idsSetKey()], [raw, entity.id]);
616
+ if (!Array.isArray(result) || result.length < 2) {
617
+ throw new Error("RedisAccessCatalogStore.insertIfAbsent received an invalid Redis response.");
618
+ }
619
+ created = String(result[0]) === "1";
620
+ const persistedRaw = String(result[1]);
621
+ const parsed = safeJsonParse(persistedRaw, {
622
+ store: `authz:${this.kind}`,
623
+ key: entity.id,
624
+ log: this.log
625
+ });
626
+ if (!parsed)
627
+ throw new Error("RedisAccessCatalogStore.insertIfAbsent could not parse the winning row.");
628
+ const mutable = winner;
629
+ for (const key of Object.keys(mutable))
630
+ delete mutable[key];
631
+ Object.assign(mutable, parsed);
632
+ })();
633
+ write.then(() => {
634
+ if (!created)
635
+ return;
636
+ this.emit({
637
+ op: "set",
638
+ kind: this.kind,
639
+ entity: winner,
640
+ previous: undefined
641
+ });
642
+ }, (error) => {
643
+ this.log?.warn("authz catalog store: atomic insert-if-absent failed", {
644
+ "ion.operation": `authz.catalog.${this.kind}.insert_if_absent`,
645
+ "ion.outcome": "error",
646
+ id: entity.id,
647
+ error: error instanceof Error ? error.message : String(error)
648
+ });
649
+ });
650
+ return readyCatalogEntry(winner, write);
651
+ }
652
+ async delete(id) {
653
+ const previous = await this.get(id);
654
+ const txn = new RedisTxn;
655
+ txn.del(this.entityKey(id));
656
+ txn.srem(this.idsSetKey(), id);
657
+ await txn.exec(this.redis);
658
+ if (!previous)
659
+ return;
660
+ this.emit({
661
+ op: "delete",
662
+ kind: this.kind,
663
+ entity: previous
664
+ });
665
+ }
666
+ async list(options) {
667
+ const ids = await this.redis.smembers(this.idsSetKey());
668
+ if (ids.length === 0)
669
+ return applyListOptions([], options);
670
+ const values = await this.redis.mget(ids.map((id) => this.entityKey(id)));
671
+ const items = values.filter((value) => value != null).map((value) => safeJsonParse(value, { store: `authz:${this.kind}`, log: this.log })).filter((parsed) => parsed !== undefined).map((parsed) => parsed);
672
+ return applyListOptions(items, options);
673
+ }
674
+ }
675
+ // packages/redis-core/src/access-index.ts
676
+ import { accessRefId as accessRefId2, assertRequiredSubjectType as assertRequiredSubjectType2 } from "@ionite/core";
677
+ var APP = "__app__";
678
+ var CHECK_WARNING_THRESHOLD = 100;
679
+ function normalizeStringArray(value) {
680
+ if (!Array.isArray(value))
681
+ return [];
682
+ return value.map((item) => item == null ? "" : String(item)).filter((item) => item.length > 0);
683
+ }
684
+ var MATERIALIZE_DIFF_SCRIPT = `local rootId=ARGV[1]
51
685
  local A=tonumber(ARGV[2])
52
686
  local R=tonumber(ARGV[3])
53
687
  local rootKey=KEYS[1]
@@ -96,7 +730,8 @@ for i=0,R-1 do
96
730
  end
97
731
  redis.call('SREM',rootKey,descriptor)
98
732
  end
99
- return 'ok'`,Y$=`local rootId=ARGV[1]
733
+ return 'ok'`;
734
+ var TEARDOWN_ROOT_SCRIPT = `local rootId=ARGV[1]
100
735
  local n=tonumber(ARGV[2])
101
736
  for i=0,n-1 do
102
737
  local kb=1+i*5
@@ -120,7 +755,8 @@ for i=0,n-1 do
120
755
  end
121
756
  end
122
757
  redis.call('DEL',KEYS[1])
123
- return 'ok'`,W$=`local n=tonumber(ARGV[1])
758
+ return 'ok'`;
759
+ var CLEAR_SUBJECT_SCRIPT = `local n=tonumber(ARGV[1])
124
760
  local roots={}
125
761
  local seen={}
126
762
  for i=0,n-1 do
@@ -139,8 +775,10 @@ for i=0,n-1 do
139
775
  redis.call('DEL',ak)
140
776
  end
141
777
  redis.call('DEL',KEYS[1])
142
- return roots`,G$=`for i=1,#KEYS do redis.call('DEL',KEYS[i]) end
143
- return 'ok'`,V$=`local subjectMember=ARGV[1]
778
+ return roots`;
779
+ var DEL_KEYS_SCRIPT = `for i=1,#KEYS do redis.call('DEL',KEYS[i]) end
780
+ return 'ok'`;
781
+ var REPLACE_SUBJECT_SCRIPT = `local subjectMember=ARGV[1]
144
782
  local C=tonumber(ARGV[2])
145
783
  local R=tonumber(ARGV[3])
146
784
  local D=tonumber(ARGV[4])
@@ -173,4 +811,1676 @@ if D==0 then
173
811
  else
174
812
  redis.call('SADD',KEYS[2],subjectMember)
175
813
  end
176
- return 'ok'`;function T(Z,$){return`${$}:${Z}`}function b(Z){return Z&&Z.length>0?Z:y}function o(Z){return JSON.stringify([Z.subject,Z.targetType,Z.target,Z.rToken])}function j(Z){let $;try{$=JSON.parse(Z)}catch{$=[]}let[Q,Y,W,V]=Array.isArray($)?$:[];return{subject:Q??"",targetType:Y??"permission",target:W??"",rToken:V??y}}class FZ{redis;prefix;log;constructor(Z){this.redis=Z.redis,this.prefix=_(Z,"access-index"),this.log=F(Z)}key(...Z){return C(this.prefix,...Z)}subjectKeyPart(Z){let $=Z.indexOf(":");if($<=0)return X(Z);return`${Z.slice(0,$)}:${X(Z.slice($+1))}`}hasKey(Z,$,Q,Y){return this.key("idx:has",this.subjectKeyPart(Z),$,X(Q),X(Y))}anyKey(Z,$,Q){return this.key("idx:any",this.subjectKeyPart(Z),$,X(Q))}edgeRootsKey(Z,$,Q,Y){return this.key("idx:er",this.subjectKeyPart(Z),$,X(Q),X(Y))}tripleKey(Z,$,Q){return this.key("idx:tr",this.subjectKeyPart(Z),$,X(Q))}rootKey(Z){return this.key("idx:root",X(Z))}subjectKey(Z){return this.key("idx:subj",this.subjectKeyPart(Z))}subjectsKey(){return this.key("idx:subjects")}disabledKey(Z){return this.key("idx:disabled",this.subjectKeyPart(Z))}async has(Z,$,Q,Y,W){w(W);let V=R(Z),G=R(Q);if(!V||!G)return{allow:!1};let B=T(V,W);if(Y===void 0){let[O,A]=await this.redis.mget([this.disabledKey(B),this.anyKey(B,$,G)]);if(O!=null)return{allow:!1,metadata:{reason:"subject_disabled"}};return A!=null?this.allow(V,W,$,G,void 0):{allow:!1}}let U=R(Y),J=await this.redis.mget([this.disabledKey(B),this.hasKey(B,$,G,b(U)),this.hasKey(B,$,G,y)]);if(J[0]!=null)return{allow:!1,metadata:{reason:"subject_disabled"}};if(J[1]!=null)return this.allow(V,W,$,G,U);if(J[2]!=null)return this.allow(V,W,$,G,void 0);return{allow:!1}}async hasGroup(Z,$,Q,Y){return this.has(Z,"group",$,Q,Y)}async hasRole(Z,$,Q,Y){return this.has(Z,"role",$,Q,Y)}async hasPermission(Z,$,Q,Y){return this.has(Z,"permission",$,Q,Y)}async hasAny(Z,$,Q){if($.length===0)return{allow:!1};this.warnLargeCheck("redis.access_index.has_any",$.length);let Y=await this.evaluateTargets(Z,$,Q);for(let W of Y)if(W.allow)return W;return{allow:!1}}async hasAll(Z,$,Q){if($.length===0){let V=R(Z);if(V){if(await this.redis.get(this.disabledKey(T(V,Q)))!=null)return{allow:!1,metadata:{reason:"subject_disabled"}}}return{allow:!0}}this.warnLargeCheck("redis.access_index.has_all",$.length);let Y=await this.evaluateTargets(Z,$,Q),W=[];for(let V of Y){if(!V.allow)return{allow:!1,matched:W};if(V.matched)W.push(...V.matched)}return{allow:!0,matched:W}}async evaluateTargets(Z,$,Q){let Y=R(Z);if(!Y)return $.map(()=>({allow:!1}));let W=T(Y,Q),V=[this.disabledKey(W)],G=$.map((U)=>{let J=R(U.target);if(!J)return{kind:"invalid"};if(U.resource===void 0){let P=V.push(this.anyKey(W,U.targetType,J))-1;return{kind:"any",targetType:U.targetType,targetId:J,idx:P}}let O=R(U.resource),A=V.push(this.hasKey(W,U.targetType,J,b(O)))-1,q=V.push(this.hasKey(W,U.targetType,J,y))-1;return{kind:"resource",targetType:U.targetType,targetId:J,resourceId:O,idxResource:A,idxApp:q}}),B=await this.redis.mget(V);if(B[0]!=null)return $.map(()=>({allow:!1,metadata:{reason:"subject_disabled"}}));return G.map((U)=>{if(U.kind==="invalid")return{allow:!1};if(U.kind==="any")return B[U.idx]!=null?this.allow(Y,Q,U.targetType,U.targetId,void 0):{allow:!1};if(B[U.idxResource]!=null)return this.allow(Y,Q,U.targetType,U.targetId,U.resourceId);if(B[U.idxApp]!=null)return this.allow(Y,Q,U.targetType,U.targetId,void 0);return{allow:!1}})}edgeKeys(Z){return[this.edgeRootsKey(Z.subject,Z.targetType,Z.target,Z.rToken),this.hasKey(Z.subject,Z.targetType,Z.target,Z.rToken),this.tripleKey(Z.subject,Z.targetType,Z.target),this.anyKey(Z.subject,Z.targetType,Z.target),this.subjectKey(Z.subject)]}async materialize(Z,$){let Q=new Map;for(let G of $){let B=R(G.subject),U=R(G.target);if(!B||!U)continue;w(G.subjectType,"Effective edge subjectType");let J=T(B,G.subjectType),O=b(G.resource),A={subject:J,targetType:G.targetType,target:U,rToken:O};Q.set(o(A),A)}let Y=new Set(await this.redis.smembers(this.rootKey(Z))),W=[];for(let[G,B]of Q)if(!Y.has(G))W.push({encoded:G,d:B});let V=[];for(let G of Y)if(!Q.has(G))V.push({encoded:G,d:j(G)});if(W.length===0&&V.length===0)return;if(E(this.redis)){let G=[this.rootKey(Z),this.subjectsKey()],B=[Z,String(W.length),String(V.length)];for(let{encoded:U,d:J}of W)G.push(...this.edgeKeys(J)),B.push(U,J.rToken,J.subject);for(let{encoded:U,d:J}of V)G.push(...this.edgeKeys(J)),B.push(U,J.rToken);await this.redis.runScript(Q$,G,B);return}for(let{encoded:G,d:B}of W)await this.redis.sadd(this.edgeRootsKey(B.subject,B.targetType,B.target,B.rToken),Z),await this.redis.set(this.hasKey(B.subject,B.targetType,B.target,B.rToken),"1"),await this.redis.sadd(this.tripleKey(B.subject,B.targetType,B.target),B.rToken),await this.redis.set(this.anyKey(B.subject,B.targetType,B.target),"1"),await this.redis.sadd(this.rootKey(Z),G),await this.redis.sadd(this.subjectKey(B.subject),G),await this.redis.sadd(this.subjectsKey(),B.subject);for(let{encoded:G,d:B}of V)await this.dropRootFromEdge(Z,B.subject,B.targetType,B.target,B.rToken),await this.redis.srem(this.rootKey(Z),G)}async replaceSubject(Z,$,Q){if(!E(this.redis))throw Error("RedisAccessIndex.replaceSubject requires an adapter with atomic runScript support.");let Y=R(Z);if(!Y)return;let W=T(Y,$),V=[];for(let q of Q){let P=R(q.subject);w(q.subjectType,"Effective edge subjectType");let k=q.subjectType;if(P!==Y||k!==$)throw Error(`AccessIndex.replaceSubject received an edge for ${k}:${P}; expected ${W}.`);let f=R(q.target);if(!f)continue;let S={subject:W,targetType:q.targetType,target:f,rToken:b(q.resource)};V.push({rootId:q.rootId,encoded:o(S),d:S})}let B=(await this.redis.smembers(this.subjectKey(W))).map(j),U=await Promise.all(B.map((q)=>this.redis.smembers(this.edgeRootsKey(q.subject,q.targetType,q.target,q.rToken)))),J=[...new Set(U.flat())],O=[this.subjectKey(W),this.subjectsKey()];for(let q of B){let[P,k,f,S]=this.edgeKeys(q);O.push(P,k,f,S)}for(let q of J)O.push(this.rootKey(q));for(let q of V){let[P,k,f,S]=this.edgeKeys(q.d);O.push(this.rootKey(q.rootId),P,k,f,S)}let A=[W,String(B.length),String(J.length),String(V.length)];for(let q of V)A.push(q.rootId,q.encoded,q.d.rToken);await this.redis.runScript(V$,O,A)}async removeByRoot(Z){let $=this.rootKey(Z),Q=await this.redis.smembers($);if(Q.length===0){await this.redis.del($);return}if(E(this.redis)){let Y=[$],W=[Z,String(Q.length)];for(let V of Q){let G=j(V);Y.push(...this.edgeKeys(G)),W.push(V,G.rToken)}await this.redis.runScript(Y$,Y,W);return}for(let Y of Q){let{subject:W,targetType:V,target:G,rToken:B}=j(Y);await this.dropRootFromEdge(Z,W,V,G,B)}await this.redis.del($)}async listSubjects(){let Z=await this.redis.smembers(this.subjectsKey()),$=[];for(let Q of Z){let Y=Q.indexOf(":");if(Y<=0)throw Error(`AccessIndex subject key "${Q}" is missing a subjectType prefix.`);let W=Q.slice(0,Y);w(W,"AccessIndex subject key subjectType"),$.push({subjectType:W,subject:Q.slice(Y+1)})}return $}async setSubjectDisabled(Z,$,Q){let Y=R(Z);if(!Y)return;let W=this.disabledKey(T(Y,$));if(Q)await this.redis.set(W,"1");else await this.redis.del(W)}async isSubjectDisabled(Z,$){w($);let Q=R(Z);if(!Q)return!1;return await this.redis.get(this.disabledKey(T(Q,$)))!=null}async clear(Z,$){if(Z===void 0){let W=await this.redis.smembers(this.subjectsKey());for(let V of W)await this.clearSubject(V);await this.redis.del(this.subjectsKey());return}w($);let Q=R(Z);if(!Q)return;let Y=T(Q,$);await this.clearSubject(Y),await this.redis.srem(this.subjectsKey(),Y)}async clearSubject(Z){let $=this.subjectKey(Z),Q=await this.redis.smembers($);if(Q.length===0){await this.redis.del($);return}if(E(this.redis)){let Y=[$];for(let V of Q){let{targetType:G,target:B,rToken:U}=j(V);Y.push(this.edgeRootsKey(Z,G,B,U),this.hasKey(Z,G,B,U),this.tripleKey(Z,G,B),this.anyKey(Z,G,B))}let W=$$(await this.redis.runScript(W$,Y,[String(Q.length)]));if(W.length>0)await this.redis.runScript(G$,W.map((V)=>this.rootKey(V)),[]);return}for(let Y of Q){let{targetType:W,target:V,rToken:G}=j(Y),B=this.edgeRootsKey(Z,W,V,G),U=await this.redis.smembers(B);for(let O of U)await this.redis.srem(this.rootKey(O),Y);if(await this.redis.del(B),await this.redis.del(this.hasKey(Z,W,V,G)),await this.redis.srem(this.tripleKey(Z,W,V),G),(await this.redis.smembers(this.tripleKey(Z,W,V))).length===0)await this.redis.del(this.tripleKey(Z,W,V)),await this.redis.del(this.anyKey(Z,W,V))}await this.redis.del($)}async dropRootFromEdge(Z,$,Q,Y,W){let V=this.edgeRootsKey($,Q,Y,W);if(await this.redis.srem(V,Z),(await this.redis.smembers(V)).length>0)return;await this.redis.del(V),await this.redis.del(this.hasKey($,Q,Y,W));let B=o({subject:$,targetType:Q,target:Y,rToken:W});if(await this.redis.srem(this.subjectKey($),B),await this.redis.srem(this.tripleKey($,Q,Y),W),(await this.redis.smembers(this.tripleKey($,Q,Y))).length===0)await this.redis.del(this.tripleKey($,Q,Y)),await this.redis.del(this.anyKey($,Q,Y))}allow(Z,$,Q,Y,W){return{allow:!0,matched:[{subject:Z,subjectType:$,targetType:Q,target:Y,resources:W?[W]:void 0}]}}warnLargeCheck(Z,$){if($<=qZ)return;this.log.warn("Redis access index check received a large bounded target set; issuing per-target reads.",{"ion.operation":Z,"ion.outcome":"degraded",count:$,threshold:qZ})}}var B$=512;async function U$(Z,$,Q=512){if(Z.length===0)return[];let Y=Q>0?Q:512,W=[];for(let V=0;V<Z.length;V+=Y){let G=await $(Z.slice(V,V+Y));for(let B of G)W.push(B??null)}return W}import{list as H$,StoreConflictError as J$}from"@ionite/core";var M$="customer:ids",O$="customer:name:",z$="customer:external:";function X$(Z){let $=Z.createdAt??new Date,Q=Z.updatedAt??$,Y={...Z,createdAt:$.toISOString(),updatedAt:Q.toISOString()};if(Y.ciam&&typeof Y.ciam==="object"&&Y.ciam!==null){let W=Y.ciam;if(W.expires instanceof Date)W.expires=W.expires.toISOString()}return JSON.stringify(Y)}function n(Z,$){let Q=L(Z,{store:"customer",log:$});if(Q===void 0)return;if(Q.ciam&&typeof Q.ciam==="object"&&Q.ciam!==null){let Y=Q.ciam;if(typeof Y.expires==="string")Y.expires=new Date(Y.expires)}return{...Q,createdAt:typeof Q.createdAt==="string"?new Date(Q.createdAt):void 0,updatedAt:typeof Q.updatedAt==="string"?new Date(Q.updatedAt):void 0}}class DZ{redis;prefix;log;constructor(Z){this.redis=Z.redis,this.prefix=_(Z,"customer-store"),this.log=F(Z)}key(...Z){return C(this.prefix,...Z)}entityKey(Z){return this.key("customer",X(Z))}idsSetKey(){return this.key(M$)}nameIndexKey(Z){return this.key(O$+X(Z))}externalIdIndexKey(Z){return this.key(z$+X(Z))}async get(Z){let $=await this.redis.get(this.entityKey(Z));if($==null)return;return n($,this.log)}async lookup(Z){if(Z.id){let $=await this.get(Z.id);if($)return $}if(Z.userName){let $=await this.redis.get(this.nameIndexKey(Z.userName.toLowerCase()));if($!=null)return this.get($)}if(Z.externalId)return this.getByExternalId(Z.externalId);return}async getByExternalId(Z){let $=await this.redis.get(this.externalIdIndexKey(Z));if($==null)return;return this.get($)}async upsert(Z){let $=Z.id?.trim()||crypto.randomUUID(),Q={...Z,id:$},Y=this.entityKey($),W=await this.redis.get(Y),V=W!=null?n(W,this.log)??null:null,G=Q.userName?.toLowerCase(),B=V?.userName?.toLowerCase(),U=Q.externalId,J=V?.externalId,O=new D;if(G)O.claimUnique(this.nameIndexKey(G),$);if(U)O.claimUnique(this.externalIdIndexKey(U),$);if(B&&B!==G)O.del(this.nameIndexKey(B));if(J&&J!==U)O.del(this.externalIdIndexKey(J));if(O.set(Y,X$(Q)),G)O.set(this.nameIndexKey(G),$);if(U)O.set(this.externalIdIndexKey(U),$);if(O.sadd(this.idsSetKey(),$),await O.exec(this.redis)==="conflict")throw new J$(`userName "${Q.userName}" is already in use`,"unique_violation","userName");return Q}async delete(Z){let $=await this.get(Z);if($==null)return 0;let Q=new D;if(Q.del(this.entityKey(Z)),$.userName)Q.del(this.nameIndexKey($.userName.toLowerCase()));if($.externalId)Q.del(this.externalIdIndexKey($.externalId));return Q.srem(this.idsSetKey(),Z),await Q.exec(this.redis),1}async list(Z){let $=h(Z),Q=(await this.redis.smembers(this.idsSetKey())).sort();if(Q.length===0)return H$(0,[],$.start??0,$.limit);let Y=Q.map((G)=>this.entityKey(G)),V=(await this.redis.mget(Y)).filter((G)=>G!=null).map((G)=>n(G,this.log)).filter((G)=>G!=null);return K(V,$)}}import{createHash as q$}from"node:crypto";function LZ(Z){return q$("sha1").update(Z,"utf8").digest("hex")}function F$(Z){return(Z instanceof Error?Z.message:String(Z)).includes("NOSCRIPT")}function D$(Z){let $=new Map;return async(Q,Y,W)=>{let V=$.get(Q);if(!V)V=LZ(Q),$.set(Q,V);try{return await Z.evalsha(V,Y,W)}catch(G){if(F$(G))return Z.eval(Q,Y,W);throw G}}}function _Z(Z){return typeof Z==="object"&&Z!==null&&!Array.isArray(Z)}function L$(Z){let $=_Z(Z.clientOptions)?{...Z.clientOptions}:_Z(Z)?{...Z}:{};return delete $.redisUrl,delete $.clientOptions,{redisUrl:(typeof Z.redisUrl==="string"?Z.redisUrl.trim():"")||void 0,clientOptions:$}}import{defaultLogger as _$}from"@ionite/core";function CZ(Z,$){return`${Z}:${encodeURIComponent($)}`}function C$(Z,$){if(typeof Z!=="object"||Z===null||Array.isArray(Z))return!1;let Q=Z,Y=Q.headers;return Q.kind==="delivery"&&typeof Q.key==="string"&&typeof Q.rawBody==="string"&&typeof Y==="object"&&Y!==null&&Y.requestId===$&&typeof Y.keyId==="string"&&typeof Y.signature==="string"}function N$(Z,$={}){if(!Z.getdel)throw Error("Redis adapter must implement getdel() for LFV callback coordination");let Q=Z.getdel.bind(Z),Y=$.channelPrefix?.trim()||"ionite:lfv:otp",W=$.logger??_$;return{async put(V,G,B){if(G.kind!=="delivery"||G.headers.requestId!==V)throw Error("LFV callback mailbox only accepts matching delivery envelopes");let U=Math.max(1,Math.ceil(B/1000));await Z.setex(CZ(Y,V),U,JSON.stringify(G))},async take(V){let G=await Q(CZ(Y,V));if(G==null)return;let B;try{B=JSON.parse(G)}catch(U){W.warn("Invalid JSON received from LFV Redis callback mailbox",{"ion.operation":"lfv.callback.redis.take","ion.outcome":"rejected",requestId:V,error:U instanceof Error?U.message:String(U)});return}if(!C$(B,V)){W.warn("Invalid envelope received from LFV Redis callback mailbox",{"ion.operation":"lfv.callback.redis.take","ion.outcome":"rejected",requestId:V});return}return B}}}import{StoreConflictError as A$}from"@ionite/core";function P$(Z){return JSON.stringify({token:Z.token,customer:Z.customer,createdAt:Z.createdAt.toISOString(),expiresAt:Z.expiresAt.toISOString()})}function NZ(Z,$){let Q=L(Z,{store:"magic-link",log:$});if(Q===void 0)return;return{...Q,createdAt:new Date(Q.createdAt),expiresAt:new Date(Q.expiresAt)}}class AZ{redis;prefix;log;constructor(Z){this.redis=Z.redis,this.prefix=_(Z,"magic-link-store"),this.log=F(Z)}key(Z){return C(this.prefix,"magic",X(Z))}async create(Z,$,Q){let Y=this.key(Z),W={token:Z,customer:$,createdAt:new Date,expiresAt:Q},V=Math.max(1,Math.floor((Q.getTime()-Date.now())/1000));if(await m(this.redis,Y,P$(W),{ttlSeconds:V})==="conflict")throw new A$(`Magic link with token ${Z} already exists`,"already_exists","token")}async get(Z){let $=await this.redis.get(this.key(Z));if($==null)return null;return NZ($,this.log)??null}async getAndDelete(Z){let $=this.key(Z),Q;if(typeof this.redis.getdel==="function")Q=await this.redis.getdel($);else if(Q=await this.redis.get($),Q!=null)await this.redis.del($);if(Q==null)return null;let Y=NZ(Q,this.log);if(Y==null)return null;if(Date.now()>Y.expiresAt.getTime())return null;return Y}async delete(Z){await this.redis.del(this.key(Z))}}import{defaultLogger as K$}from"@ionite/core";function R$(Z,$){if(typeof $!=="number"||$<=0)return Z;return new Promise((Q,Y)=>{let W=setTimeout(()=>{Y(Error(`Reactive Redis adapter did not become ready within ${$}ms`))},$);Z.then(()=>{clearTimeout(W),Q()},(V)=>{clearTimeout(W),Y(V)})})}function E$(){let Z,$;return{promise:new Promise((Y,W)=>{Z=Y,$=W}),resolve:Z,reject:$}}function PZ(Z){return typeof Z==="function"}function f$(Z){let $=Z.logger??K$,Q=Z.sourceName?.trim()||"redis",Y=Z.path.trim(),W,V,G=!1,B,U,J,O=!1,A=!1,q=!1,P=new Map,k=Promise.resolve(),f=E$();f.promise.catch(()=>{});let S={async get(H){return x((M)=>M.get(H))},async set(H,M){await x((z)=>z.set(H,M))},async setex(H,M,z){await x((N)=>N.setex(H,M,z))},async del(H){await x((M)=>M.del(H))},async sadd(H,...M){await x((z)=>z.sadd(H,...M))},async srem(H,...M){await x((z)=>z.srem(H,...M))},async smembers(H){return x((M)=>M.smembers(H))},async mget(H){return x((M)=>M.mget(H))},async publish(H,M){await x(async(z)=>{if(!z.publish)throw Error("Active Redis adapter does not support publish (Pub/Sub)");await z.publish(H,M)})},async subscribeChannel(H,M){let z=P.get(H);if(!z){let v=new Set;z={listeners:v,dispatch:(yZ)=>{for(let gZ of Array.from(v))try{gZ(yZ)}catch(u){$.warn("Redis channel listener threw",{"ion.operation":"redis.adapter.subscribe","ion.outcome":"failure",sourceName:Q,path:Y,channel:H,error:u instanceof Error?u.message:String(u)})}}},P.set(H,z)}let N=z;N.listeners.add(M);try{await jZ(H,N)}catch(v){if(N.listeners.delete(M),N.listeners.size===0)P.delete(H);throw v}return async()=>{if(N.listeners.delete(M),N.listeners.size===0){P.delete(H);let v=N.unsubscribe;N.unsubscribe=void 0,await v?.()}}},ready(H){return R$(f.promise,H)},isReady(){return W!==void 0},async close(){if(O=!0,e(),!G)G=!0,f.reject(Error(`Reactive Redis adapter for source "${Q}" at path "${Y}" was closed before becoming ready.`));await k.catch(()=>{}),await g(W),W=void 0;for(let H of P.values())H.unsubscribe=void 0;P.clear(),l()}};function r(H){k=k.then(H).catch((M)=>{if(!G)G=!0,f.reject(M);$.warn("Reactive Redis adapter update failed",{"ion.operation":"redis.adapter.update","ion.outcome":"degraded",sourceName:Q,path:Y,error:M instanceof Error?M.message:String(M)})})}async function t(){if(O)throw Error(`Reactive Redis adapter for source "${Q}" at path "${Y}" is closed.`);let H=W?.adapter;if(H)return H;if(await k.catch(()=>{}),O)throw Error(`Reactive Redis adapter for source "${Q}" at path "${Y}" is closed.`);let M=W?.adapter;if(M)return M;if(await f.promise,O)throw Error(`Reactive Redis adapter for source "${Q}" at path "${Y}" is closed.`);let z=W?.adapter;if(!z)throw Error(`Reactive Redis adapter for source "${Q}" at path "${Y}" did not become ready.`);return z}async function x(H){let M=await t();return H(M)}function e(){V?.(),V=void 0}async function jZ(H,M){if(M.unsubscribe)return;if(!M.attaching)M.attaching=(async()=>{let z=await t();if(!z.subscribeChannel)throw Error("Active Redis adapter does not support subscribeChannel (Pub/Sub)");if(M.listeners.size===0)return;M.unsubscribe=await z.subscribeChannel(H,M.dispatch)})().finally(()=>{M.attaching=void 0});await M.attaching}async function mZ(H){for(let[M,z]of P){if(z.attaching)await z.attaching.catch(()=>{});if(z.unsubscribe=void 0,!H.subscribeChannel){$.warn("Active Redis adapter does not support subscribeChannel; channel subscriptions dropped.",{"ion.operation":"redis.adapter.capabilities","ion.outcome":"degraded",sourceName:Q,path:Y,capability:"subscribeChannel"});return}try{z.unsubscribe=await H.subscribeChannel(M,z.dispatch)}catch(N){$.warn("Failed to re-subscribe Redis channel after client rotation",{"ion.operation":"redis.adapter.subscribe","ion.outcome":"failure",sourceName:Q,path:Y,channel:M,error:N instanceof Error?N.message:String(N)})}}}async function IZ(H){e(),V=await H.subscribe(Y,(M)=>{r(async()=>{if(O)return;await bZ(M)})},Z.subscription??Z.secretOptions)}async function bZ(H){if(O)return;let M=`${Q}:${Y}:${H.metadata.version}`;if(W!==void 0&&U===M&&B===H.metadata.version)return;let z=Z.endpointOf?.(H);if(W!==void 0&&z!=null&&z!==""&&J!=null&&J!==""&&z!==J)throw Error(`Reactive Redis adapter for source "${Q}" at path "${Y}" rejected a rotation that changed the endpoint (${J} -> ${z}); only credentials may rotate under an active adapter.`);let N=await Z.createClient(H);if(O){await g(N);return}let v=W;if(W=N,U=M,B=H.metadata.version,z!=null&&z!=="")J=z;if(l(),v!==void 0)await mZ(N.adapter);if(!G)G=!0,f.resolve();await g(v)}async function g(H){if(!H?.dispose)return;await H.dispose()}function l(){let H=W?.adapter;if(H?.runScript&&PZ(H.runScript))S.runScript=async(M,z,N)=>{return x(async(v)=>{if(!v.runScript)throw Error("Active Redis adapter does not support runScript");return v.runScript(M,z,N)})};else if(delete S.runScript,H!==void 0&&!A)A=!0,$.warn("Active Redis adapter does not support runScript (EVAL); stores fall back to non-isolated writes.",{"ion.operation":"redis.adapter.capabilities","ion.outcome":"degraded",sourceName:Q,path:Y,capability:"runScript"});if(H?.getdel&&PZ(H.getdel))S.getdel=async(M)=>{return x(async(z)=>{if(!z.getdel)return null;return z.getdel(M)})};else if(delete S.getdel,H!==void 0&&!q)q=!0,$.warn("Active Redis adapter does not support getdel (GETDEL); single-use consume falls back to get+del.",{"ion.operation":"redis.adapter.capabilities","ion.outcome":"degraded",sourceName:Q,path:Y,capability:"getdel"})}return l(),r(async()=>{if(O)return;await IZ(Z.source)}),S}import{StoreConflictError as S$}from"@ionite/core";var v$=5;function k$(Z,$){let Q;try{Q=JSON.parse(Z)}catch{return}if(Q==null||typeof Q!=="object"||Array.isArray(Q))return;return JSON.stringify({...Q,...$})}function KZ(Z){return JSON.stringify({...Z,createdAt:Z.createdAt.toISOString()})}function RZ(Z,$){let Q=L(Z,{store:"session",log:$});if(Q===void 0)return;return{...Q,createdAt:new Date(Q.createdAt)}}function EZ(Z){if(!Z)return;let $=Z instanceof Date?Z.getTime():new Date(Z).getTime();if(Number.isNaN($))return;let Q=Math.ceil(($-Date.now())/1000);return Q>0?Q:1}class fZ{redis;prefix;log;constructor(Z){this.redis=Z.redis,this.prefix=_(Z,"session-store"),this.log=F(Z)}key(Z){return C(this.prefix,"session",X(Z))}async create(Z){let $=this.key(Z.sid),Q=EZ(Z.expiresAt);if(await m(this.redis,$,KZ(Z),{ttlSeconds:Q})==="conflict")throw new S$(`Session with sid ${Z.sid} already exists`,"already_exists","sid")}async get(Z){let $=await this.redis.get(this.key(Z));if($==null)return null;return RZ($,this.log)??null}async update(Z,$){let Q=this.key(Z),Y=JSON.parse(JSON.stringify($)),W="expiresAt"in Y?EZ(Y.expiresAt):void 0;if(E(this.redis))for(let J=0;J<v$;J++){let O=await this.redis.get(Q);if(O==null)throw Error(`Session with sid ${Z} not found`);let A=k$(O,Y);if(A===void 0)throw Error(`Session with sid ${Z} not found`);let q=await c(this.redis,Q,O,A,W);if(q==="ok")return;if(q==="missing")throw Error(`Session with sid ${Z} not found`)}let V=await this.redis.get(Q);if(V==null)throw Error(`Session with sid ${Z} not found`);let G=RZ(V,this.log);if(G==null)throw Error(`Session with sid ${Z} not found`);let B={...G,...$},U=KZ(B);if(W!=null&&W>0)await this.redis.setex(Q,Math.max(1,Math.floor(W)),U);else await this.redis.set(Q,U)}async delete(Z){await this.redis.del(this.key(Z))}}var x$="tenant:ids";function h$(Z){return JSON.stringify({...Z,createdAt:Z.createdAt.toISOString(),updatedAt:Z.updatedAt.toISOString()})}function SZ(Z,$){let Q=L(Z,{store:"tenant",log:$});if(Q===void 0)return;return{...Q,createdAt:new Date(Q.createdAt),updatedAt:new Date(Q.updatedAt)}}class vZ{redis;prefix;log;constructor(Z){this.redis=Z.redis,this.prefix=_(Z,"tenant-store"),this.log=F(Z)}key(...Z){return C(this.prefix,...Z)}entityKey(Z){return this.key("tenant",X(Z))}idsSetKey(){return this.key(x$)}async get(Z){let $=await this.redis.get(this.entityKey(Z));if($==null)return;return SZ($,this.log)}async upsert(Z){return await new D().set(this.entityKey(Z.id),h$(Z)).sadd(this.idsSetKey(),Z.id).exec(this.redis),Z}async list(Z){let $=await this.redis.smembers(this.idsSetKey());if($.length===0)return K([],Z);let Q=$.map((V)=>this.entityKey(V)),W=(await this.redis.mget(Q)).map((V)=>V!=null?SZ(V,this.log):void 0).filter((V)=>V!=null);return K(W,Z)}async delete(Z){let $=await this.get(Z);return await new D().srem(this.idsSetKey(),Z).del(this.entityKey(Z)).exec(this.redis),$?1:0}}class kZ{store;ionCache;constructor(Z){this.ionCache=Z.ionCache,this.store=new vZ(Z)}async get(Z){return this.store.get(Z)}async upsert(Z){let $=await this.store.upsert(Z);return this.ionCache?.delete(Z.id),$}async list(Z){return this.store.list(Z)}async delete(Z){let $=await this.store.delete(Z);return this.ionCache?.delete(Z),$}async getIon(Z){if(!this.ionCache)throw Error(`${this.constructor.name}.getIon() requires an app-owned ionCache. Pass { ionCache } (an lru-cache instance whose fetchMethod builds the tenant Ionite and whose dispose `+"closes it) — the SDK does not cache tenant runtimes. See the multi-tenant docs.");return this.ionCache.fetch(Z)}}import{list as T$,StoreConflictError as xZ}from"@ionite/core";var w$="user:ids",j$="user:name:",m$="user:external-id:";function I$(Z){let $=Z.createdAt??new Date,Q=Z.updatedAt??$,Y={...Z,createdAt:$.toISOString(),updatedAt:Q.toISOString()};if(Y.sso&&typeof Y.sso==="object"&&Y.sso!==null){let W=Y.sso;if(W.expires instanceof Date)W.expires=W.expires.toISOString()}return JSON.stringify(Y)}function a(Z,$){let Q=L(Z,{store:"user",log:$});if(Q===void 0)return;if(Q.sso&&typeof Q.sso==="object"&&Q.sso!==null){let Y=Q.sso;if(typeof Y.expires==="string")Y.expires=new Date(Y.expires)}return{...Q,createdAt:typeof Q.createdAt==="string"?new Date(Q.createdAt):void 0,updatedAt:typeof Q.updatedAt==="string"?new Date(Q.updatedAt):void 0}}class hZ{redis;prefix;log;constructor(Z){this.redis=Z.redis,this.prefix=_(Z,"user-store"),this.log=F(Z)}key(...Z){return C(this.prefix,...Z)}entityKey(Z){return this.key("user",X(Z))}idsSetKey(){return this.key(w$)}nameIndexKey(Z){return this.key(j$+X(Z))}externalIdIndexKey(Z){return this.key(m$+X(Z))}async get(Z){let $=await this.redis.get(this.entityKey(Z));if($==null)return;return a($,this.log)}async getByExternalId(Z){let $=await this.redis.get(this.externalIdIndexKey(Z));return $?this.get($):void 0}async lookup(Z){return this.get(Z.id??"")}async upsert(Z){let $=Z.id?.trim()||crypto.randomUUID(),Q={...Z,id:$},Y=this.entityKey($),W=await this.redis.get(Y),V=W!=null?a(W,this.log)??null:null,G=Q.userName?.toLowerCase(),B=V?.userName?.toLowerCase(),U=Q.externalId,J=V?.externalId,O=new D;if(G)O.claimUnique(this.nameIndexKey(G),$);if(U)O.claimUnique(this.externalIdIndexKey(U),$);if(B&&B!==G)O.del(this.nameIndexKey(B));if(J&&J!==U)O.del(this.externalIdIndexKey(J));if(O.set(Y,I$(Q)),G)O.set(this.nameIndexKey(G),$);if(U)O.set(this.externalIdIndexKey(U),$);if(O.sadd(this.idsSetKey(),$),await O.exec(this.redis)==="conflict"){if(U){let A=await this.redis.get(this.externalIdIndexKey(U));if(A&&A!==$)throw new xZ(`externalId "${U}" is already in use`,"unique_violation","externalId")}throw new xZ(`userName "${Q.userName}" is already in use`,"unique_violation","userName")}return Q}async delete(Z){let $=await this.get(Z);if($==null)return 0;let Q=new D;if(Q.del(this.entityKey(Z)),$.userName)Q.del(this.nameIndexKey($.userName.toLowerCase()));if($.externalId)Q.del(this.externalIdIndexKey($.externalId));return Q.srem(this.idsSetKey(),Z),await Q.exec(this.redis),1}async list(Z){let $=h(Z),Q=(await this.redis.smembers(this.idsSetKey())).sort();if(Q.length===0)return T$(0,[],$.start??0,$.limit);let Y=Q.map((G)=>this.entityKey(G)),V=(await this.redis.mget(Y)).filter((G)=>G!=null).map((G)=>a(G,this.log)).filter((G)=>G!=null);return K(V,$)}}import{list as b$,StoreConflictError as y$}from"@ionite/core";var g$="workload-identity:ids",l$="workload-identity:client:",u$="workload-identity:external:";function p$(Z){let $=Z.createdAt??new Date,Q=Z.updatedAt??$;return JSON.stringify({...Z,createdAt:$.toISOString(),updatedAt:Q.toISOString()})}function s(Z,$){let Q=L(Z,{store:"workload-identity",log:$});if(Q===void 0)return;return{...Q,createdAt:typeof Q.createdAt==="string"?new Date(Q.createdAt):void 0,updatedAt:typeof Q.updatedAt==="string"?new Date(Q.updatedAt):void 0}}class TZ{redis;prefix;log;constructor(Z){this.redis=Z.redis,this.prefix=_(Z,"workload-identity-store"),this.log=F(Z)}key(...Z){return C(this.prefix,...Z)}entityKey(Z){return this.key("workload-identity",X(Z))}idsSetKey(){return this.key(g$)}clientIndexKey(Z){return this.key(l$+X(Z))}externalIdIndexKey(Z){return this.key(u$+X(Z))}async get(Z){let $=await this.redis.get(this.entityKey(Z));if($==null)return;return s($,this.log)}async lookup(Z){if(Z.id){let $=await this.get(Z.id);if($)return $}if(Z.clientId){let $=await this.redis.get(this.clientIndexKey(Z.clientId));if($!=null)return this.get($)}if(Z.externalId)return this.getByExternalId(Z.externalId);return}async getByExternalId(Z){let $=await this.redis.get(this.externalIdIndexKey(Z));if($==null)return;return this.get($)}async upsert(Z){let $=Z.id?.trim()||crypto.randomUUID(),Q=this.entityKey($),Y=await this.redis.get(Q),W=null;if(Y!=null)W=s(Y,this.log)??null;let V=new Date,G={...Z,id:$,createdAt:W?.createdAt??Z.createdAt??V,updatedAt:V},B=new D;if(G.clientId)B.claimUnique(this.clientIndexKey(G.clientId),$);if(G.externalId)B.claimUnique(this.externalIdIndexKey(G.externalId),$);if(W?.clientId&&W.clientId!==G.clientId)B.del(this.clientIndexKey(W.clientId));if(W?.externalId&&W.externalId!==G.externalId)B.del(this.externalIdIndexKey(W.externalId));if(B.set(Q,p$(G)),G.clientId)B.set(this.clientIndexKey(G.clientId),$);if(G.externalId)B.set(this.externalIdIndexKey(G.externalId),$);if(B.sadd(this.idsSetKey(),$),await B.exec(this.redis)==="conflict")throw new y$(`clientId "${G.clientId}" is already in use`,"unique_violation","clientId");return G}async delete(Z){let $=await this.get(Z);if($==null)return 0;let Q=new D;if(Q.del(this.entityKey(Z)),$.clientId)Q.del(this.clientIndexKey($.clientId));if($.externalId)Q.del(this.externalIdIndexKey($.externalId));return Q.srem(this.idsSetKey(),Z),await Q.exec(this.redis),1}async list(Z){let $=h(Z),Q=(await this.redis.smembers(this.idsSetKey())).sort();if(Q.length===0)return b$(0,[],$.start??0,$.limit);let Y=Q.map((G)=>this.entityKey(G)),V=(await this.redis.mget(Y)).filter((G)=>G!=null).map((G)=>s(G,this.log)).filter((G)=>G!=null);return K(V,$)}}function c$(Z){return JSON.stringify({...Z,expires_at:Z.expires_at.toISOString(),created_at:Z.created_at.toISOString()})}function d$(Z,$){let Q=L(Z,{store:"workload-token",log:$});if(Q===void 0)return;return{...Q,expires_at:new Date(Q.expires_at),created_at:new Date(Q.created_at)}}class wZ{redis;prefix;log;constructor(Z){this.redis=Z.redis,this.prefix=_(Z,"workload-token-store"),this.log=F(Z)}key(Z){return C(this.prefix,"workload","token",X(Z))}async set(Z){let $=this.key(Z.workload_id),Q=Math.max(1,Math.floor((Z.expires_at.getTime()-Date.now())/1000));await this.redis.setex($,Q,c$(Z))}async get(Z){let $=await this.redis.get(this.key(Z));if($==null)return null;let Q=d$($,this.log);if(Q==null)return null;if(Date.now()>Q.expires_at.getTime())return await this.redis.del(this.key(Z)),null;return Q}async delete(Z){await this.redis.del(this.key(Z))}async isValid(Z){return await this.get(Z)!==null}async cleanup(){}}export{E as supportsRunScript,L$ as splitIonRedisSecret,LZ as scriptSha1,C as prefixedKey,F as getLogger,_ as getKeyPrefix,X as encodeKeyId,N$ as createRedisLfvTransport,f$ as createReactiveRedis,D$ as createEvalshaRunner,U$ as chunkedMget,m as atomicCreate,c as atomicCompareAndSet,wZ as RedisWorkloadTokenStore,TZ as RedisWorkloadIdentityStore,hZ as RedisUserStore,D as RedisTxn,kZ as RedisTenantStore,fZ as RedisSessionStore,XZ as RedisResourceStore,AZ as RedisMagicLinkStore,DZ as RedisCustomerStore,FZ as RedisAccessIndex,zZ as RedisAccessCatalogStore,JZ as RedisAccessAssignmentStore,QZ as MAX_LIST_LIMIT,B$ as DEFAULT_MGET_CHUNK_SIZE,$Z as DEFAULT_LIST_LIMIT};
814
+ return 'ok'`;
815
+ function namespacedSubject(subjectId, subjectType) {
816
+ return `${subjectType}:${subjectId}`;
817
+ }
818
+ function resourceToken(resource) {
819
+ return resource && resource.length > 0 ? resource : APP;
820
+ }
821
+ function encodeDescriptor(d) {
822
+ return JSON.stringify([d.subject, d.targetType, d.target, d.rToken]);
823
+ }
824
+ function decodeDescriptor(value) {
825
+ let parts;
826
+ try {
827
+ parts = JSON.parse(value);
828
+ } catch {
829
+ parts = [];
830
+ }
831
+ const [subject, targetType, target, rToken] = Array.isArray(parts) ? parts : [];
832
+ return {
833
+ subject: subject ?? "",
834
+ targetType: targetType ?? "permission",
835
+ target: target ?? "",
836
+ rToken: rToken ?? APP
837
+ };
838
+ }
839
+
840
+ class RedisAccessIndex {
841
+ redis;
842
+ prefix;
843
+ log;
844
+ constructor(options) {
845
+ this.redis = options.redis;
846
+ this.prefix = getKeyPrefix(options, "access-index");
847
+ this.log = getLogger(options);
848
+ }
849
+ key(...parts) {
850
+ return prefixedKey(this.prefix, ...parts);
851
+ }
852
+ subjectKeyPart(ns) {
853
+ const sep = ns.indexOf(":");
854
+ if (sep <= 0)
855
+ return encodeKeyId(ns);
856
+ return `${ns.slice(0, sep)}:${encodeKeyId(ns.slice(sep + 1))}`;
857
+ }
858
+ hasKey(subject, targetType, target, rToken) {
859
+ return this.key("idx:has", this.subjectKeyPart(subject), targetType, encodeKeyId(target), encodeKeyId(rToken));
860
+ }
861
+ anyKey(subject, targetType, target) {
862
+ return this.key("idx:any", this.subjectKeyPart(subject), targetType, encodeKeyId(target));
863
+ }
864
+ edgeRootsKey(subject, targetType, target, rToken) {
865
+ return this.key("idx:er", this.subjectKeyPart(subject), targetType, encodeKeyId(target), encodeKeyId(rToken));
866
+ }
867
+ tripleKey(subject, targetType, target) {
868
+ return this.key("idx:tr", this.subjectKeyPart(subject), targetType, encodeKeyId(target));
869
+ }
870
+ rootKey(rootId) {
871
+ return this.key("idx:root", encodeKeyId(rootId));
872
+ }
873
+ subjectKey(subject) {
874
+ return this.key("idx:subj", this.subjectKeyPart(subject));
875
+ }
876
+ subjectsKey() {
877
+ return this.key("idx:subjects");
878
+ }
879
+ disabledKey(subject) {
880
+ return this.key("idx:disabled", this.subjectKeyPart(subject));
881
+ }
882
+ async has(subject, targetType, target, resource, subjectType) {
883
+ assertRequiredSubjectType2(subjectType);
884
+ const subjectId = accessRefId2(subject);
885
+ const targetId = accessRefId2(target);
886
+ if (!subjectId || !targetId)
887
+ return { allow: false };
888
+ const ns = namespacedSubject(subjectId, subjectType);
889
+ if (resource === undefined) {
890
+ const [disabled, present] = await this.redis.mget([this.disabledKey(ns), this.anyKey(ns, targetType, targetId)]);
891
+ if (disabled != null)
892
+ return { allow: false, metadata: { reason: "subject_disabled" } };
893
+ return present != null ? this.allow(subjectId, subjectType, targetType, targetId, undefined) : { allow: false };
894
+ }
895
+ const resourceId = accessRefId2(resource);
896
+ const values = await this.redis.mget([
897
+ this.disabledKey(ns),
898
+ this.hasKey(ns, targetType, targetId, resourceToken(resourceId)),
899
+ this.hasKey(ns, targetType, targetId, APP)
900
+ ]);
901
+ if (values[0] != null)
902
+ return { allow: false, metadata: { reason: "subject_disabled" } };
903
+ if (values[1] != null)
904
+ return this.allow(subjectId, subjectType, targetType, targetId, resourceId);
905
+ if (values[2] != null)
906
+ return this.allow(subjectId, subjectType, targetType, targetId, undefined);
907
+ return { allow: false };
908
+ }
909
+ async hasGroup(subject, group, resource, subjectType) {
910
+ return this.has(subject, "group", group, resource, subjectType);
911
+ }
912
+ async hasRole(subject, role, resource, subjectType) {
913
+ return this.has(subject, "role", role, resource, subjectType);
914
+ }
915
+ async hasPermission(subject, permission, resource, subjectType) {
916
+ return this.has(subject, "permission", permission, resource, subjectType);
917
+ }
918
+ async hasAny(subject, targets, subjectType) {
919
+ if (targets.length === 0)
920
+ return { allow: false };
921
+ this.warnLargeCheck("redis.access_index.has_any", targets.length);
922
+ const decisions = await this.evaluateTargets(subject, targets, subjectType);
923
+ for (const decision of decisions) {
924
+ if (decision.allow)
925
+ return decision;
926
+ }
927
+ return { allow: false };
928
+ }
929
+ async hasAll(subject, targets, subjectType) {
930
+ if (targets.length === 0) {
931
+ const subjectId = accessRefId2(subject);
932
+ if (subjectId) {
933
+ const disabled = await this.redis.get(this.disabledKey(namespacedSubject(subjectId, subjectType)));
934
+ if (disabled != null)
935
+ return { allow: false, metadata: { reason: "subject_disabled" } };
936
+ }
937
+ return { allow: true };
938
+ }
939
+ this.warnLargeCheck("redis.access_index.has_all", targets.length);
940
+ const decisions = await this.evaluateTargets(subject, targets, subjectType);
941
+ const matched = [];
942
+ for (const decision of decisions) {
943
+ if (!decision.allow)
944
+ return { allow: false, matched };
945
+ if (decision.matched)
946
+ matched.push(...decision.matched);
947
+ }
948
+ return { allow: true, matched };
949
+ }
950
+ async evaluateTargets(subject, targets, subjectType) {
951
+ const subjectId = accessRefId2(subject);
952
+ if (!subjectId)
953
+ return targets.map(() => ({ allow: false }));
954
+ const ns = namespacedSubject(subjectId, subjectType);
955
+ const flatKeys = [this.disabledKey(ns)];
956
+ const plans = targets.map((target) => {
957
+ const targetId = accessRefId2(target.target);
958
+ if (!targetId)
959
+ return { kind: "invalid" };
960
+ if (target.resource === undefined) {
961
+ const idx = flatKeys.push(this.anyKey(ns, target.targetType, targetId)) - 1;
962
+ return { kind: "any", targetType: target.targetType, targetId, idx };
963
+ }
964
+ const resourceId = accessRefId2(target.resource);
965
+ const idxResource = flatKeys.push(this.hasKey(ns, target.targetType, targetId, resourceToken(resourceId))) - 1;
966
+ const idxApp = flatKeys.push(this.hasKey(ns, target.targetType, targetId, APP)) - 1;
967
+ return { kind: "resource", targetType: target.targetType, targetId, resourceId, idxResource, idxApp };
968
+ });
969
+ const values = await this.redis.mget(flatKeys);
970
+ if (values[0] != null) {
971
+ return targets.map(() => ({ allow: false, metadata: { reason: "subject_disabled" } }));
972
+ }
973
+ return plans.map((plan) => {
974
+ if (plan.kind === "invalid")
975
+ return { allow: false };
976
+ if (plan.kind === "any") {
977
+ return values[plan.idx] != null ? this.allow(subjectId, subjectType, plan.targetType, plan.targetId, undefined) : { allow: false };
978
+ }
979
+ if (values[plan.idxResource] != null) {
980
+ return this.allow(subjectId, subjectType, plan.targetType, plan.targetId, plan.resourceId);
981
+ }
982
+ if (values[plan.idxApp] != null) {
983
+ return this.allow(subjectId, subjectType, plan.targetType, plan.targetId, undefined);
984
+ }
985
+ return { allow: false };
986
+ });
987
+ }
988
+ edgeKeys(d) {
989
+ return [
990
+ this.edgeRootsKey(d.subject, d.targetType, d.target, d.rToken),
991
+ this.hasKey(d.subject, d.targetType, d.target, d.rToken),
992
+ this.tripleKey(d.subject, d.targetType, d.target),
993
+ this.anyKey(d.subject, d.targetType, d.target),
994
+ this.subjectKey(d.subject)
995
+ ];
996
+ }
997
+ async materialize(rootId, edges) {
998
+ const desired = new Map;
999
+ for (const edge of edges) {
1000
+ const subjectId = accessRefId2(edge.subject);
1001
+ const target = accessRefId2(edge.target);
1002
+ if (!subjectId || !target)
1003
+ continue;
1004
+ assertRequiredSubjectType2(edge.subjectType, "Effective edge subjectType");
1005
+ const subject = namespacedSubject(subjectId, edge.subjectType);
1006
+ const rToken = resourceToken(edge.resource);
1007
+ const d = { subject, targetType: edge.targetType, target, rToken };
1008
+ desired.set(encodeDescriptor(d), d);
1009
+ }
1010
+ const currentEncoded = new Set(await this.redis.smembers(this.rootKey(rootId)));
1011
+ const toAdd = [];
1012
+ for (const [encoded, d] of desired) {
1013
+ if (!currentEncoded.has(encoded))
1014
+ toAdd.push({ encoded, d });
1015
+ }
1016
+ const toRemove = [];
1017
+ for (const encoded of currentEncoded) {
1018
+ if (!desired.has(encoded))
1019
+ toRemove.push({ encoded, d: decodeDescriptor(encoded) });
1020
+ }
1021
+ if (toAdd.length === 0 && toRemove.length === 0)
1022
+ return;
1023
+ if (supportsRunScript(this.redis)) {
1024
+ const keys = [this.rootKey(rootId), this.subjectsKey()];
1025
+ const args = [rootId, String(toAdd.length), String(toRemove.length)];
1026
+ for (const { encoded, d } of toAdd) {
1027
+ keys.push(...this.edgeKeys(d));
1028
+ args.push(encoded, d.rToken, d.subject);
1029
+ }
1030
+ for (const { encoded, d } of toRemove) {
1031
+ keys.push(...this.edgeKeys(d));
1032
+ args.push(encoded, d.rToken);
1033
+ }
1034
+ await this.redis.runScript(MATERIALIZE_DIFF_SCRIPT, keys, args);
1035
+ return;
1036
+ }
1037
+ for (const { encoded, d } of toAdd) {
1038
+ await this.redis.sadd(this.edgeRootsKey(d.subject, d.targetType, d.target, d.rToken), rootId);
1039
+ await this.redis.set(this.hasKey(d.subject, d.targetType, d.target, d.rToken), "1");
1040
+ await this.redis.sadd(this.tripleKey(d.subject, d.targetType, d.target), d.rToken);
1041
+ await this.redis.set(this.anyKey(d.subject, d.targetType, d.target), "1");
1042
+ await this.redis.sadd(this.rootKey(rootId), encoded);
1043
+ await this.redis.sadd(this.subjectKey(d.subject), encoded);
1044
+ await this.redis.sadd(this.subjectsKey(), d.subject);
1045
+ }
1046
+ for (const { encoded, d } of toRemove) {
1047
+ await this.dropRootFromEdge(rootId, d.subject, d.targetType, d.target, d.rToken);
1048
+ await this.redis.srem(this.rootKey(rootId), encoded);
1049
+ }
1050
+ }
1051
+ async replaceSubject(subject, subjectType, edges) {
1052
+ if (!supportsRunScript(this.redis)) {
1053
+ throw new Error("RedisAccessIndex.replaceSubject requires an adapter with atomic runScript support.");
1054
+ }
1055
+ const subjectId = accessRefId2(subject);
1056
+ if (!subjectId)
1057
+ return;
1058
+ const ns = namespacedSubject(subjectId, subjectType);
1059
+ const desired = [];
1060
+ for (const edge of edges) {
1061
+ const edgeSubjectId = accessRefId2(edge.subject);
1062
+ assertRequiredSubjectType2(edge.subjectType, "Effective edge subjectType");
1063
+ const edgeSubjectType = edge.subjectType;
1064
+ if (edgeSubjectId !== subjectId || edgeSubjectType !== subjectType) {
1065
+ throw new Error(`AccessIndex.replaceSubject received an edge for ${edgeSubjectType}:${edgeSubjectId}; expected ${ns}.`);
1066
+ }
1067
+ const target = accessRefId2(edge.target);
1068
+ if (!target)
1069
+ continue;
1070
+ const d = {
1071
+ subject: ns,
1072
+ targetType: edge.targetType,
1073
+ target,
1074
+ rToken: resourceToken(edge.resource)
1075
+ };
1076
+ desired.push({ rootId: edge.rootId, encoded: encodeDescriptor(d), d });
1077
+ }
1078
+ const currentEncoded = await this.redis.smembers(this.subjectKey(ns));
1079
+ const currentDescriptors = currentEncoded.map(decodeDescriptor);
1080
+ const currentRootSets = await Promise.all(currentDescriptors.map((d) => this.redis.smembers(this.edgeRootsKey(d.subject, d.targetType, d.target, d.rToken))));
1081
+ const currentRootIds = [...new Set(currentRootSets.flat())];
1082
+ const keys = [this.subjectKey(ns), this.subjectsKey()];
1083
+ for (const d of currentDescriptors) {
1084
+ const [edgeRoots, has, triple, any] = this.edgeKeys(d);
1085
+ keys.push(edgeRoots, has, triple, any);
1086
+ }
1087
+ for (const rootId of currentRootIds)
1088
+ keys.push(this.rootKey(rootId));
1089
+ for (const item of desired) {
1090
+ const [edgeRoots, has, triple, any] = this.edgeKeys(item.d);
1091
+ keys.push(this.rootKey(item.rootId), edgeRoots, has, triple, any);
1092
+ }
1093
+ const args = [
1094
+ ns,
1095
+ String(currentDescriptors.length),
1096
+ String(currentRootIds.length),
1097
+ String(desired.length)
1098
+ ];
1099
+ for (const item of desired)
1100
+ args.push(item.rootId, item.encoded, item.d.rToken);
1101
+ await this.redis.runScript(REPLACE_SUBJECT_SCRIPT, keys, args);
1102
+ }
1103
+ async removeByRoot(rootId) {
1104
+ const rootKey = this.rootKey(rootId);
1105
+ const members = await this.redis.smembers(rootKey);
1106
+ if (members.length === 0) {
1107
+ await this.redis.del(rootKey);
1108
+ return;
1109
+ }
1110
+ if (supportsRunScript(this.redis)) {
1111
+ const keys = [rootKey];
1112
+ const args = [rootId, String(members.length)];
1113
+ for (const member of members) {
1114
+ const d = decodeDescriptor(member);
1115
+ keys.push(...this.edgeKeys(d));
1116
+ args.push(member, d.rToken);
1117
+ }
1118
+ await this.redis.runScript(TEARDOWN_ROOT_SCRIPT, keys, args);
1119
+ return;
1120
+ }
1121
+ for (const member of members) {
1122
+ const { subject, targetType, target, rToken } = decodeDescriptor(member);
1123
+ await this.dropRootFromEdge(rootId, subject, targetType, target, rToken);
1124
+ }
1125
+ await this.redis.del(rootKey);
1126
+ }
1127
+ async listSubjects() {
1128
+ const namespaced = await this.redis.smembers(this.subjectsKey());
1129
+ const subjects = [];
1130
+ for (const ns of namespaced) {
1131
+ const sep = ns.indexOf(":");
1132
+ if (sep <= 0) {
1133
+ throw new Error(`AccessIndex subject key "${ns}" is missing a subjectType prefix.`);
1134
+ }
1135
+ const subjectType = ns.slice(0, sep);
1136
+ assertRequiredSubjectType2(subjectType, "AccessIndex subject key subjectType");
1137
+ subjects.push({ subjectType, subject: ns.slice(sep + 1) });
1138
+ }
1139
+ return subjects;
1140
+ }
1141
+ async setSubjectDisabled(subject, subjectType, disabled) {
1142
+ const subjectId = accessRefId2(subject);
1143
+ if (!subjectId)
1144
+ return;
1145
+ const key = this.disabledKey(namespacedSubject(subjectId, subjectType));
1146
+ if (disabled)
1147
+ await this.redis.set(key, "1");
1148
+ else
1149
+ await this.redis.del(key);
1150
+ }
1151
+ async isSubjectDisabled(subject, subjectType) {
1152
+ assertRequiredSubjectType2(subjectType);
1153
+ const subjectId = accessRefId2(subject);
1154
+ if (!subjectId)
1155
+ return false;
1156
+ const value = await this.redis.get(this.disabledKey(namespacedSubject(subjectId, subjectType)));
1157
+ return value != null;
1158
+ }
1159
+ async clear(subject, subjectType) {
1160
+ if (subject === undefined) {
1161
+ const subjects = await this.redis.smembers(this.subjectsKey());
1162
+ for (const s of subjects)
1163
+ await this.clearSubject(s);
1164
+ await this.redis.del(this.subjectsKey());
1165
+ return;
1166
+ }
1167
+ assertRequiredSubjectType2(subjectType);
1168
+ const subjectId = accessRefId2(subject);
1169
+ if (!subjectId)
1170
+ return;
1171
+ const ns = namespacedSubject(subjectId, subjectType);
1172
+ await this.clearSubject(ns);
1173
+ await this.redis.srem(this.subjectsKey(), ns);
1174
+ }
1175
+ async clearSubject(subjectId) {
1176
+ const subjectKey = this.subjectKey(subjectId);
1177
+ const descriptors = await this.redis.smembers(subjectKey);
1178
+ if (descriptors.length === 0) {
1179
+ await this.redis.del(subjectKey);
1180
+ return;
1181
+ }
1182
+ if (supportsRunScript(this.redis)) {
1183
+ const keys = [subjectKey];
1184
+ for (const descriptor of descriptors) {
1185
+ const { targetType, target, rToken } = decodeDescriptor(descriptor);
1186
+ keys.push(this.edgeRootsKey(subjectId, targetType, target, rToken), this.hasKey(subjectId, targetType, target, rToken), this.tripleKey(subjectId, targetType, target), this.anyKey(subjectId, targetType, target));
1187
+ }
1188
+ const rootIds = normalizeStringArray(await this.redis.runScript(CLEAR_SUBJECT_SCRIPT, keys, [String(descriptors.length)]));
1189
+ if (rootIds.length > 0) {
1190
+ await this.redis.runScript(DEL_KEYS_SCRIPT, rootIds.map((rootId) => this.rootKey(rootId)), []);
1191
+ }
1192
+ return;
1193
+ }
1194
+ for (const descriptor of descriptors) {
1195
+ const { targetType, target, rToken } = decodeDescriptor(descriptor);
1196
+ const edgeRootsKey = this.edgeRootsKey(subjectId, targetType, target, rToken);
1197
+ const rootIds = await this.redis.smembers(edgeRootsKey);
1198
+ for (const rootId of rootIds) {
1199
+ await this.redis.srem(this.rootKey(rootId), descriptor);
1200
+ }
1201
+ await this.redis.del(edgeRootsKey);
1202
+ await this.redis.del(this.hasKey(subjectId, targetType, target, rToken));
1203
+ await this.redis.srem(this.tripleKey(subjectId, targetType, target), rToken);
1204
+ const remainingTokens = await this.redis.smembers(this.tripleKey(subjectId, targetType, target));
1205
+ if (remainingTokens.length === 0) {
1206
+ await this.redis.del(this.tripleKey(subjectId, targetType, target));
1207
+ await this.redis.del(this.anyKey(subjectId, targetType, target));
1208
+ }
1209
+ }
1210
+ await this.redis.del(subjectKey);
1211
+ }
1212
+ async dropRootFromEdge(rootId, subject, targetType, target, rToken) {
1213
+ const edgeRootsKey = this.edgeRootsKey(subject, targetType, target, rToken);
1214
+ await this.redis.srem(edgeRootsKey, rootId);
1215
+ const remainingRoots = await this.redis.smembers(edgeRootsKey);
1216
+ if (remainingRoots.length > 0)
1217
+ return;
1218
+ await this.redis.del(edgeRootsKey);
1219
+ await this.redis.del(this.hasKey(subject, targetType, target, rToken));
1220
+ const descriptor = encodeDescriptor({ subject, targetType, target, rToken });
1221
+ await this.redis.srem(this.subjectKey(subject), descriptor);
1222
+ await this.redis.srem(this.tripleKey(subject, targetType, target), rToken);
1223
+ const remainingTokens = await this.redis.smembers(this.tripleKey(subject, targetType, target));
1224
+ if (remainingTokens.length === 0) {
1225
+ await this.redis.del(this.tripleKey(subject, targetType, target));
1226
+ await this.redis.del(this.anyKey(subject, targetType, target));
1227
+ }
1228
+ }
1229
+ allow(subject, subjectType, targetType, target, resource) {
1230
+ return {
1231
+ allow: true,
1232
+ matched: [{ subject, subjectType, targetType, target, resources: resource ? [resource] : undefined }]
1233
+ };
1234
+ }
1235
+ warnLargeCheck(operation, count) {
1236
+ if (count <= CHECK_WARNING_THRESHOLD)
1237
+ return;
1238
+ this.log.warn("Redis access index check received a large bounded target set; issuing per-target reads.", {
1239
+ "ion.operation": operation,
1240
+ "ion.outcome": "degraded",
1241
+ count,
1242
+ threshold: CHECK_WARNING_THRESHOLD
1243
+ });
1244
+ }
1245
+ }
1246
+ // packages/redis-core/src/adapter-helpers.ts
1247
+ var DEFAULT_MGET_CHUNK_SIZE = 512;
1248
+ async function chunkedMget(keys, fetch, chunkSize = DEFAULT_MGET_CHUNK_SIZE) {
1249
+ if (keys.length === 0)
1250
+ return [];
1251
+ const size = chunkSize > 0 ? chunkSize : DEFAULT_MGET_CHUNK_SIZE;
1252
+ const out = [];
1253
+ for (let i = 0;i < keys.length; i += size) {
1254
+ const part = await fetch(keys.slice(i, i + size));
1255
+ for (const value of part)
1256
+ out.push(value ?? null);
1257
+ }
1258
+ return out;
1259
+ }
1260
+ // packages/redis-core/src/customer-store.ts
1261
+ import {
1262
+ list as list3,
1263
+ StoreConflictError
1264
+ } from "@ionite/core";
1265
+ var IDS_SET2 = "customer:ids";
1266
+ var NAME_INDEX_PREFIX = "customer:name:";
1267
+ var EXTERNAL_ID_INDEX_PREFIX = "customer:external:";
1268
+ function serializeCustomer(customer) {
1269
+ const createdAt = customer.createdAt ?? new Date;
1270
+ const updatedAt = customer.updatedAt ?? createdAt;
1271
+ const obj = {
1272
+ ...customer,
1273
+ createdAt: createdAt.toISOString(),
1274
+ updatedAt: updatedAt.toISOString()
1275
+ };
1276
+ if (obj.ciam && typeof obj.ciam === "object" && obj.ciam !== null) {
1277
+ const ciam = obj.ciam;
1278
+ if (ciam.expires instanceof Date)
1279
+ ciam.expires = ciam.expires.toISOString();
1280
+ }
1281
+ return JSON.stringify(obj);
1282
+ }
1283
+ function reviveCustomer(data, log) {
1284
+ const raw = safeJsonParse(data, { store: "customer", log });
1285
+ if (raw === undefined)
1286
+ return;
1287
+ if (raw.ciam && typeof raw.ciam === "object" && raw.ciam !== null) {
1288
+ const ciam = raw.ciam;
1289
+ if (typeof ciam.expires === "string")
1290
+ ciam.expires = new Date(ciam.expires);
1291
+ }
1292
+ return {
1293
+ ...raw,
1294
+ createdAt: typeof raw.createdAt === "string" ? new Date(raw.createdAt) : undefined,
1295
+ updatedAt: typeof raw.updatedAt === "string" ? new Date(raw.updatedAt) : undefined
1296
+ };
1297
+ }
1298
+
1299
+ class RedisCustomerStore {
1300
+ redis;
1301
+ prefix;
1302
+ log;
1303
+ constructor(options) {
1304
+ this.redis = options.redis;
1305
+ this.prefix = getKeyPrefix(options, "customer-store");
1306
+ this.log = getLogger(options);
1307
+ }
1308
+ key(...parts) {
1309
+ return prefixedKey(this.prefix, ...parts);
1310
+ }
1311
+ entityKey(id) {
1312
+ return this.key("customer", encodeKeyId(id));
1313
+ }
1314
+ idsSetKey() {
1315
+ return this.key(IDS_SET2);
1316
+ }
1317
+ nameIndexKey(normalized) {
1318
+ return this.key(NAME_INDEX_PREFIX + encodeKeyId(normalized));
1319
+ }
1320
+ externalIdIndexKey(externalId) {
1321
+ return this.key(EXTERNAL_ID_INDEX_PREFIX + encodeKeyId(externalId));
1322
+ }
1323
+ async get(sub) {
1324
+ const raw = await this.redis.get(this.entityKey(sub));
1325
+ if (raw == null)
1326
+ return;
1327
+ return reviveCustomer(raw, this.log);
1328
+ }
1329
+ async lookup(customer) {
1330
+ if (customer.id) {
1331
+ const byId = await this.get(customer.id);
1332
+ if (byId)
1333
+ return byId;
1334
+ }
1335
+ if (customer.userName) {
1336
+ const id = await this.redis.get(this.nameIndexKey(customer.userName.toLowerCase()));
1337
+ if (id != null)
1338
+ return this.get(id);
1339
+ }
1340
+ if (customer.externalId) {
1341
+ return this.getByExternalId(customer.externalId);
1342
+ }
1343
+ return;
1344
+ }
1345
+ async getByExternalId(externalId) {
1346
+ const id = await this.redis.get(this.externalIdIndexKey(externalId));
1347
+ if (id == null)
1348
+ return;
1349
+ return this.get(id);
1350
+ }
1351
+ async upsert(customer) {
1352
+ const customerId = customer.id?.trim() || crypto.randomUUID();
1353
+ const stored = { ...customer, id: customerId };
1354
+ const k = this.entityKey(customerId);
1355
+ const existingRaw = await this.redis.get(k);
1356
+ const existing = existingRaw != null ? reviveCustomer(existingRaw, this.log) ?? null : null;
1357
+ const newName = stored.userName?.toLowerCase();
1358
+ const oldName = existing?.userName?.toLowerCase();
1359
+ const newExternalId = stored.externalId;
1360
+ const oldExternalId = existing?.externalId;
1361
+ const txn = new RedisTxn;
1362
+ if (newName)
1363
+ txn.claimUnique(this.nameIndexKey(newName), customerId);
1364
+ if (newExternalId)
1365
+ txn.claimUnique(this.externalIdIndexKey(newExternalId), customerId);
1366
+ if (oldName && oldName !== newName)
1367
+ txn.del(this.nameIndexKey(oldName));
1368
+ if (oldExternalId && oldExternalId !== newExternalId)
1369
+ txn.del(this.externalIdIndexKey(oldExternalId));
1370
+ txn.set(k, serializeCustomer(stored));
1371
+ if (newName)
1372
+ txn.set(this.nameIndexKey(newName), customerId);
1373
+ if (newExternalId)
1374
+ txn.set(this.externalIdIndexKey(newExternalId), customerId);
1375
+ txn.sadd(this.idsSetKey(), customerId);
1376
+ if (await txn.exec(this.redis) === "conflict") {
1377
+ throw new StoreConflictError(`userName "${stored.userName}" is already in use`, "unique_violation", "userName");
1378
+ }
1379
+ return stored;
1380
+ }
1381
+ async delete(sub) {
1382
+ const customer = await this.get(sub);
1383
+ if (customer == null)
1384
+ return 0;
1385
+ const txn = new RedisTxn;
1386
+ txn.del(this.entityKey(sub));
1387
+ if (customer.userName)
1388
+ txn.del(this.nameIndexKey(customer.userName.toLowerCase()));
1389
+ if (customer.externalId)
1390
+ txn.del(this.externalIdIndexKey(customer.externalId));
1391
+ txn.srem(this.idsSetKey(), sub);
1392
+ await txn.exec(this.redis);
1393
+ return 1;
1394
+ }
1395
+ async list(options) {
1396
+ const opts = normalizeListLimit(options);
1397
+ const ids = (await this.redis.smembers(this.idsSetKey())).sort();
1398
+ if (ids.length === 0)
1399
+ return list3(0, [], opts.start ?? 0, opts.limit);
1400
+ const keys = ids.map((id) => this.entityKey(id));
1401
+ const values = await this.redis.mget(keys);
1402
+ const arr = values.filter((v) => v != null).map((v) => reviveCustomer(v, this.log)).filter((c) => c != null);
1403
+ return applyListOptions(arr, opts);
1404
+ }
1405
+ }
1406
+ // packages/redis-core/src/evalsha.ts
1407
+ import { createHash as createHash2 } from "node:crypto";
1408
+ function scriptSha1(script) {
1409
+ return createHash2("sha1").update(script, "utf8").digest("hex");
1410
+ }
1411
+ function isNoScriptError(error) {
1412
+ const message = error instanceof Error ? error.message : String(error);
1413
+ return message.includes("NOSCRIPT");
1414
+ }
1415
+ function createEvalshaRunner(client) {
1416
+ const shaByScript = new Map;
1417
+ return async (script, keys, args) => {
1418
+ let sha = shaByScript.get(script);
1419
+ if (!sha) {
1420
+ sha = scriptSha1(script);
1421
+ shaByScript.set(script, sha);
1422
+ }
1423
+ try {
1424
+ return await client.evalsha(sha, keys, args);
1425
+ } catch (error) {
1426
+ if (isNoScriptError(error))
1427
+ return client.eval(script, keys, args);
1428
+ throw error;
1429
+ }
1430
+ };
1431
+ }
1432
+ // packages/redis-core/src/ion-redis-shared.ts
1433
+ function isRecord(value) {
1434
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1435
+ }
1436
+ function splitIonRedisSecret(secret) {
1437
+ const clientOptions = isRecord(secret.clientOptions) ? { ...secret.clientOptions } : isRecord(secret) ? { ...secret } : {};
1438
+ delete clientOptions.redisUrl;
1439
+ delete clientOptions.clientOptions;
1440
+ const redisUrl = typeof secret.redisUrl === "string" ? secret.redisUrl.trim() : "";
1441
+ return {
1442
+ redisUrl: redisUrl || undefined,
1443
+ clientOptions
1444
+ };
1445
+ }
1446
+ // packages/redis-core/src/lfv-callback-transport.ts
1447
+ import { defaultLogger as defaultLogger3 } from "@ionite/core";
1448
+ function channelFor(prefix, key) {
1449
+ return `${prefix}:${encodeURIComponent(key)}`;
1450
+ }
1451
+ function isEnvelope(value, expectedRequestId) {
1452
+ if (typeof value !== "object" || value === null || Array.isArray(value))
1453
+ return false;
1454
+ const envelope = value;
1455
+ const headers = envelope.headers;
1456
+ return envelope.kind === "delivery" && typeof envelope.key === "string" && typeof envelope.rawBody === "string" && typeof headers === "object" && headers !== null && headers.requestId === expectedRequestId && typeof headers.keyId === "string" && typeof headers.signature === "string";
1457
+ }
1458
+ function createRedisLfvTransport(adapter, options = {}) {
1459
+ if (!adapter.getdel) {
1460
+ throw new Error("Redis adapter must implement getdel() for LFV callback coordination");
1461
+ }
1462
+ const getdel = adapter.getdel.bind(adapter);
1463
+ const prefix = options.channelPrefix?.trim() || "ionite:lfv:otp";
1464
+ const log = options.logger ?? defaultLogger3;
1465
+ return {
1466
+ async put(requestId, envelope, ttlMs) {
1467
+ if (envelope.kind !== "delivery" || envelope.headers.requestId !== requestId) {
1468
+ throw new Error("LFV callback mailbox only accepts matching delivery envelopes");
1469
+ }
1470
+ const ttlSeconds = Math.max(1, Math.ceil(ttlMs / 1000));
1471
+ await adapter.setex(channelFor(prefix, requestId), ttlSeconds, JSON.stringify(envelope));
1472
+ },
1473
+ async take(requestId) {
1474
+ const message = await getdel(channelFor(prefix, requestId));
1475
+ if (message == null)
1476
+ return;
1477
+ let parsed;
1478
+ try {
1479
+ parsed = JSON.parse(message);
1480
+ } catch (error) {
1481
+ log.warn("Invalid JSON received from LFV Redis callback mailbox", {
1482
+ "ion.operation": "lfv.callback.redis.take",
1483
+ "ion.outcome": "rejected",
1484
+ requestId,
1485
+ error: error instanceof Error ? error.message : String(error)
1486
+ });
1487
+ return;
1488
+ }
1489
+ if (!isEnvelope(parsed, requestId)) {
1490
+ log.warn("Invalid envelope received from LFV Redis callback mailbox", {
1491
+ "ion.operation": "lfv.callback.redis.take",
1492
+ "ion.outcome": "rejected",
1493
+ requestId
1494
+ });
1495
+ return;
1496
+ }
1497
+ return parsed;
1498
+ }
1499
+ };
1500
+ }
1501
+ // packages/redis-core/src/magic-link-store.ts
1502
+ import { StoreConflictError as StoreConflictError2 } from "@ionite/core";
1503
+ function serializeMagicLink(link) {
1504
+ return JSON.stringify({
1505
+ token: link.token,
1506
+ customer: link.customer,
1507
+ createdAt: link.createdAt.toISOString(),
1508
+ expiresAt: link.expiresAt.toISOString()
1509
+ });
1510
+ }
1511
+ function reviveMagicLink(data, log) {
1512
+ const raw = safeJsonParse(data, { store: "magic-link", log });
1513
+ if (raw === undefined)
1514
+ return;
1515
+ return {
1516
+ ...raw,
1517
+ createdAt: new Date(raw.createdAt),
1518
+ expiresAt: new Date(raw.expiresAt)
1519
+ };
1520
+ }
1521
+
1522
+ class RedisMagicLinkStore {
1523
+ redis;
1524
+ prefix;
1525
+ log;
1526
+ constructor(options) {
1527
+ this.redis = options.redis;
1528
+ this.prefix = getKeyPrefix(options, "magic-link-store");
1529
+ this.log = getLogger(options);
1530
+ }
1531
+ key(token) {
1532
+ return prefixedKey(this.prefix, "magic", encodeKeyId(token));
1533
+ }
1534
+ async create(token, customer, expiresAt) {
1535
+ const k = this.key(token);
1536
+ const link = {
1537
+ token,
1538
+ customer,
1539
+ createdAt: new Date,
1540
+ expiresAt
1541
+ };
1542
+ const ttlSec = Math.max(1, Math.floor((expiresAt.getTime() - Date.now()) / 1000));
1543
+ const result = await atomicCreate(this.redis, k, serializeMagicLink(link), { ttlSeconds: ttlSec });
1544
+ if (result === "conflict") {
1545
+ throw new StoreConflictError2(`Magic link with token ${token} already exists`, "already_exists", "token");
1546
+ }
1547
+ }
1548
+ async get(token) {
1549
+ const raw = await this.redis.get(this.key(token));
1550
+ if (raw == null)
1551
+ return null;
1552
+ return reviveMagicLink(raw, this.log) ?? null;
1553
+ }
1554
+ async getAndDelete(token) {
1555
+ const k = this.key(token);
1556
+ let raw;
1557
+ if (typeof this.redis.getdel === "function") {
1558
+ raw = await this.redis.getdel(k);
1559
+ } else {
1560
+ raw = await this.redis.get(k);
1561
+ if (raw != null)
1562
+ await this.redis.del(k);
1563
+ }
1564
+ if (raw == null)
1565
+ return null;
1566
+ const link = reviveMagicLink(raw, this.log);
1567
+ if (link == null)
1568
+ return null;
1569
+ if (Date.now() > link.expiresAt.getTime())
1570
+ return null;
1571
+ return link;
1572
+ }
1573
+ async delete(token) {
1574
+ await this.redis.del(this.key(token));
1575
+ }
1576
+ }
1577
+ // packages/redis-core/src/reactive-redis.ts
1578
+ import {
1579
+ defaultLogger as defaultLogger4
1580
+ } from "@ionite/core";
1581
+ function readyWithOptionalTimeout(promise, timeout) {
1582
+ if (typeof timeout !== "number" || timeout <= 0) {
1583
+ return promise;
1584
+ }
1585
+ return new Promise((resolve, reject) => {
1586
+ const timer = setTimeout(() => {
1587
+ reject(new Error(`Reactive Redis adapter did not become ready within ${timeout}ms`));
1588
+ }, timeout);
1589
+ promise.then(() => {
1590
+ clearTimeout(timer);
1591
+ resolve();
1592
+ }, (error) => {
1593
+ clearTimeout(timer);
1594
+ reject(error);
1595
+ });
1596
+ });
1597
+ }
1598
+ function createDeferred() {
1599
+ let resolve;
1600
+ let reject;
1601
+ const promise = new Promise((nextResolve, nextReject) => {
1602
+ resolve = nextResolve;
1603
+ reject = nextReject;
1604
+ });
1605
+ return { promise, resolve, reject };
1606
+ }
1607
+ function isFunction(value) {
1608
+ return typeof value === "function";
1609
+ }
1610
+ function createReactiveRedis(options) {
1611
+ const log = options.logger ?? defaultLogger4;
1612
+ const sourceName = options.sourceName?.trim() || "redis";
1613
+ const secretPath = options.path.trim();
1614
+ let currentClient;
1615
+ let unsubscribe;
1616
+ let readyResolved = false;
1617
+ let lastAppliedVersion;
1618
+ let lastConfiguredSignature;
1619
+ let lastEndpoint;
1620
+ let closed = false;
1621
+ let warnedNoRunScript = false;
1622
+ let warnedNoGetdel = false;
1623
+ const channelSubscriptions = new Map;
1624
+ let operationQueue = Promise.resolve();
1625
+ const readyState = createDeferred();
1626
+ readyState.promise.catch(() => {});
1627
+ const reactive = {
1628
+ async get(key) {
1629
+ return withAdapter((adapter) => adapter.get(key));
1630
+ },
1631
+ async set(key, value) {
1632
+ await withAdapter((adapter) => adapter.set(key, value));
1633
+ },
1634
+ async setex(key, seconds, value) {
1635
+ await withAdapter((adapter) => adapter.setex(key, seconds, value));
1636
+ },
1637
+ async del(key) {
1638
+ await withAdapter((adapter) => adapter.del(key));
1639
+ },
1640
+ async sadd(key, ...members) {
1641
+ await withAdapter((adapter) => adapter.sadd(key, ...members));
1642
+ },
1643
+ async srem(key, ...members) {
1644
+ await withAdapter((adapter) => adapter.srem(key, ...members));
1645
+ },
1646
+ async smembers(key) {
1647
+ return withAdapter((adapter) => adapter.smembers(key));
1648
+ },
1649
+ async mget(keys) {
1650
+ return withAdapter((adapter) => adapter.mget(keys));
1651
+ },
1652
+ async publish(channel, message) {
1653
+ await withAdapter(async (adapter) => {
1654
+ if (!adapter.publish) {
1655
+ throw new Error("Active Redis adapter does not support publish (Pub/Sub)");
1656
+ }
1657
+ await adapter.publish(channel, message);
1658
+ });
1659
+ },
1660
+ async subscribeChannel(channel, listener) {
1661
+ let entry = channelSubscriptions.get(channel);
1662
+ if (!entry) {
1663
+ const listeners = new Set;
1664
+ entry = {
1665
+ listeners,
1666
+ dispatch: (message) => {
1667
+ for (const active of Array.from(listeners)) {
1668
+ try {
1669
+ active(message);
1670
+ } catch (error) {
1671
+ log.warn("Redis channel listener threw", {
1672
+ "ion.operation": "redis.adapter.subscribe",
1673
+ "ion.outcome": "failure",
1674
+ sourceName,
1675
+ path: secretPath,
1676
+ channel,
1677
+ error: error instanceof Error ? error.message : String(error)
1678
+ });
1679
+ }
1680
+ }
1681
+ }
1682
+ };
1683
+ channelSubscriptions.set(channel, entry);
1684
+ }
1685
+ const subscription = entry;
1686
+ subscription.listeners.add(listener);
1687
+ try {
1688
+ await attachChannel(channel, subscription);
1689
+ } catch (error) {
1690
+ subscription.listeners.delete(listener);
1691
+ if (subscription.listeners.size === 0)
1692
+ channelSubscriptions.delete(channel);
1693
+ throw error;
1694
+ }
1695
+ return async () => {
1696
+ subscription.listeners.delete(listener);
1697
+ if (subscription.listeners.size === 0) {
1698
+ channelSubscriptions.delete(channel);
1699
+ const unsubscribe2 = subscription.unsubscribe;
1700
+ subscription.unsubscribe = undefined;
1701
+ await unsubscribe2?.();
1702
+ }
1703
+ };
1704
+ },
1705
+ ready(timeout) {
1706
+ return readyWithOptionalTimeout(readyState.promise, timeout);
1707
+ },
1708
+ isReady() {
1709
+ return currentClient !== undefined;
1710
+ },
1711
+ async close() {
1712
+ closed = true;
1713
+ cleanupSubscription();
1714
+ if (!readyResolved) {
1715
+ readyResolved = true;
1716
+ readyState.reject(new Error(`Reactive Redis adapter for source "${sourceName}" at path "${secretPath}" was closed before becoming ready.`));
1717
+ }
1718
+ await operationQueue.catch(() => {});
1719
+ await disposeClient(currentClient);
1720
+ currentClient = undefined;
1721
+ for (const entry of channelSubscriptions.values()) {
1722
+ entry.unsubscribe = undefined;
1723
+ }
1724
+ channelSubscriptions.clear();
1725
+ syncOptionalCapabilities();
1726
+ }
1727
+ };
1728
+ function enqueue(task) {
1729
+ operationQueue = operationQueue.then(task).catch((error) => {
1730
+ if (!readyResolved) {
1731
+ readyResolved = true;
1732
+ readyState.reject(error);
1733
+ }
1734
+ log.warn("Reactive Redis adapter update failed", {
1735
+ "ion.operation": "redis.adapter.update",
1736
+ "ion.outcome": "degraded",
1737
+ sourceName,
1738
+ path: secretPath,
1739
+ error: error instanceof Error ? error.message : String(error)
1740
+ });
1741
+ });
1742
+ }
1743
+ async function waitForAdapter() {
1744
+ if (closed) {
1745
+ throw new Error(`Reactive Redis adapter for source "${sourceName}" at path "${secretPath}" is closed.`);
1746
+ }
1747
+ const current = currentClient?.adapter;
1748
+ if (current) {
1749
+ return current;
1750
+ }
1751
+ await operationQueue.catch(() => {});
1752
+ if (closed) {
1753
+ throw new Error(`Reactive Redis adapter for source "${sourceName}" at path "${secretPath}" is closed.`);
1754
+ }
1755
+ const applied = currentClient?.adapter;
1756
+ if (applied) {
1757
+ return applied;
1758
+ }
1759
+ await readyState.promise;
1760
+ if (closed) {
1761
+ throw new Error(`Reactive Redis adapter for source "${sourceName}" at path "${secretPath}" is closed.`);
1762
+ }
1763
+ const next = currentClient?.adapter;
1764
+ if (!next) {
1765
+ throw new Error(`Reactive Redis adapter for source "${sourceName}" at path "${secretPath}" did not become ready.`);
1766
+ }
1767
+ return next;
1768
+ }
1769
+ async function withAdapter(run) {
1770
+ const adapter = await waitForAdapter();
1771
+ return run(adapter);
1772
+ }
1773
+ function cleanupSubscription() {
1774
+ unsubscribe?.();
1775
+ unsubscribe = undefined;
1776
+ }
1777
+ async function attachChannel(channel, entry) {
1778
+ if (entry.unsubscribe)
1779
+ return;
1780
+ if (!entry.attaching) {
1781
+ entry.attaching = (async () => {
1782
+ const adapter = await waitForAdapter();
1783
+ if (!adapter.subscribeChannel) {
1784
+ throw new Error("Active Redis adapter does not support subscribeChannel (Pub/Sub)");
1785
+ }
1786
+ if (entry.listeners.size === 0)
1787
+ return;
1788
+ entry.unsubscribe = await adapter.subscribeChannel(channel, entry.dispatch);
1789
+ })().finally(() => {
1790
+ entry.attaching = undefined;
1791
+ });
1792
+ }
1793
+ await entry.attaching;
1794
+ }
1795
+ async function resubscribeChannels(adapter) {
1796
+ for (const [channel, entry] of channelSubscriptions) {
1797
+ if (entry.attaching)
1798
+ await entry.attaching.catch(() => {});
1799
+ entry.unsubscribe = undefined;
1800
+ if (!adapter.subscribeChannel) {
1801
+ log.warn("Active Redis adapter does not support subscribeChannel; channel subscriptions dropped.", {
1802
+ "ion.operation": "redis.adapter.capabilities",
1803
+ "ion.outcome": "degraded",
1804
+ sourceName,
1805
+ path: secretPath,
1806
+ capability: "subscribeChannel"
1807
+ });
1808
+ return;
1809
+ }
1810
+ try {
1811
+ entry.unsubscribe = await adapter.subscribeChannel(channel, entry.dispatch);
1812
+ } catch (error) {
1813
+ log.warn("Failed to re-subscribe Redis channel after client rotation", {
1814
+ "ion.operation": "redis.adapter.subscribe",
1815
+ "ion.outcome": "failure",
1816
+ sourceName,
1817
+ path: secretPath,
1818
+ channel,
1819
+ error: error instanceof Error ? error.message : String(error)
1820
+ });
1821
+ }
1822
+ }
1823
+ }
1824
+ async function subscribeToSource(source) {
1825
+ cleanupSubscription();
1826
+ unsubscribe = await source.subscribe(secretPath, (fullSecret) => {
1827
+ enqueue(async () => {
1828
+ if (closed)
1829
+ return;
1830
+ await applySecret(fullSecret);
1831
+ });
1832
+ }, options.subscription ?? options.secretOptions);
1833
+ }
1834
+ async function applySecret(fullSecret) {
1835
+ if (closed)
1836
+ return;
1837
+ const nextSignature = `${sourceName}:${secretPath}:${fullSecret.metadata.version}`;
1838
+ if (currentClient !== undefined && lastConfiguredSignature === nextSignature && lastAppliedVersion === fullSecret.metadata.version) {
1839
+ return;
1840
+ }
1841
+ const nextEndpoint = options.endpointOf?.(fullSecret);
1842
+ if (currentClient !== undefined && nextEndpoint != null && nextEndpoint !== "" && lastEndpoint != null && lastEndpoint !== "" && nextEndpoint !== lastEndpoint) {
1843
+ throw new Error(`Reactive Redis adapter for source "${sourceName}" at path "${secretPath}" rejected a rotation that changed the endpoint (${lastEndpoint} -> ${nextEndpoint}); only credentials may rotate under an active adapter.`);
1844
+ }
1845
+ const nextClient = await options.createClient(fullSecret);
1846
+ if (closed) {
1847
+ await disposeClient(nextClient);
1848
+ return;
1849
+ }
1850
+ const previousClient = currentClient;
1851
+ currentClient = nextClient;
1852
+ lastConfiguredSignature = nextSignature;
1853
+ lastAppliedVersion = fullSecret.metadata.version;
1854
+ if (nextEndpoint != null && nextEndpoint !== "")
1855
+ lastEndpoint = nextEndpoint;
1856
+ syncOptionalCapabilities();
1857
+ if (previousClient !== undefined) {
1858
+ await resubscribeChannels(nextClient.adapter);
1859
+ }
1860
+ if (!readyResolved) {
1861
+ readyResolved = true;
1862
+ readyState.resolve();
1863
+ }
1864
+ await disposeClient(previousClient);
1865
+ }
1866
+ async function disposeClient(client) {
1867
+ if (!client?.dispose)
1868
+ return;
1869
+ await client.dispose();
1870
+ }
1871
+ function syncOptionalCapabilities() {
1872
+ const adapter = currentClient?.adapter;
1873
+ if (adapter?.runScript && isFunction(adapter.runScript)) {
1874
+ reactive.runScript = async (script, keys, args) => {
1875
+ return withAdapter(async (current) => {
1876
+ if (!current.runScript)
1877
+ throw new Error("Active Redis adapter does not support runScript");
1878
+ return current.runScript(script, keys, args);
1879
+ });
1880
+ };
1881
+ } else {
1882
+ delete reactive.runScript;
1883
+ if (adapter !== undefined && !warnedNoRunScript) {
1884
+ warnedNoRunScript = true;
1885
+ log.warn("Active Redis adapter does not support runScript (EVAL); stores fall back to non-isolated writes.", {
1886
+ "ion.operation": "redis.adapter.capabilities",
1887
+ "ion.outcome": "degraded",
1888
+ sourceName,
1889
+ path: secretPath,
1890
+ capability: "runScript"
1891
+ });
1892
+ }
1893
+ }
1894
+ if (adapter?.getdel && isFunction(adapter.getdel)) {
1895
+ reactive.getdel = async (key) => {
1896
+ return withAdapter(async (current) => {
1897
+ if (!current.getdel)
1898
+ return null;
1899
+ return current.getdel(key);
1900
+ });
1901
+ };
1902
+ } else {
1903
+ delete reactive.getdel;
1904
+ if (adapter !== undefined && !warnedNoGetdel) {
1905
+ warnedNoGetdel = true;
1906
+ log.warn("Active Redis adapter does not support getdel (GETDEL); single-use consume falls back to get+del.", {
1907
+ "ion.operation": "redis.adapter.capabilities",
1908
+ "ion.outcome": "degraded",
1909
+ sourceName,
1910
+ path: secretPath,
1911
+ capability: "getdel"
1912
+ });
1913
+ }
1914
+ }
1915
+ }
1916
+ syncOptionalCapabilities();
1917
+ enqueue(async () => {
1918
+ if (closed)
1919
+ return;
1920
+ await subscribeToSource(options.source);
1921
+ });
1922
+ return reactive;
1923
+ }
1924
+ // packages/redis-core/src/session-store.ts
1925
+ import { StoreConflictError as StoreConflictError3 } from "@ionite/core";
1926
+ var SESSION_UPDATE_MAX_CAS_ATTEMPTS = 5;
1927
+ function mergeSessionRaw(raw, patch) {
1928
+ let obj;
1929
+ try {
1930
+ obj = JSON.parse(raw);
1931
+ } catch {
1932
+ return;
1933
+ }
1934
+ if (obj == null || typeof obj !== "object" || Array.isArray(obj))
1935
+ return;
1936
+ return JSON.stringify({ ...obj, ...patch });
1937
+ }
1938
+ function serializeSession(session) {
1939
+ return JSON.stringify({
1940
+ ...session,
1941
+ createdAt: session.createdAt.toISOString()
1942
+ });
1943
+ }
1944
+ function reviveSession(data, log) {
1945
+ const raw = safeJsonParse(data, { store: "session", log });
1946
+ if (raw === undefined)
1947
+ return;
1948
+ return {
1949
+ ...raw,
1950
+ createdAt: new Date(raw.createdAt)
1951
+ };
1952
+ }
1953
+ function ttlSecondsFromExpiresAt(expiresAt) {
1954
+ if (!expiresAt)
1955
+ return;
1956
+ const t = expiresAt instanceof Date ? expiresAt.getTime() : new Date(expiresAt).getTime();
1957
+ if (Number.isNaN(t))
1958
+ return;
1959
+ const seconds = Math.ceil((t - Date.now()) / 1000);
1960
+ return seconds > 0 ? seconds : 1;
1961
+ }
1962
+
1963
+ class RedisSessionStore {
1964
+ redis;
1965
+ prefix;
1966
+ log;
1967
+ constructor(options) {
1968
+ this.redis = options.redis;
1969
+ this.prefix = getKeyPrefix(options, "session-store");
1970
+ this.log = getLogger(options);
1971
+ }
1972
+ key(sid) {
1973
+ return prefixedKey(this.prefix, "session", encodeKeyId(sid));
1974
+ }
1975
+ async create(session) {
1976
+ const k = this.key(session.sid);
1977
+ const ttlSeconds = ttlSecondsFromExpiresAt(session.expiresAt);
1978
+ const result = await atomicCreate(this.redis, k, serializeSession(session), { ttlSeconds });
1979
+ if (result === "conflict") {
1980
+ throw new StoreConflictError3(`Session with sid ${session.sid} already exists`, "already_exists", "sid");
1981
+ }
1982
+ }
1983
+ async get(sid) {
1984
+ const raw = await this.redis.get(this.key(sid));
1985
+ if (raw == null)
1986
+ return null;
1987
+ return reviveSession(raw, this.log) ?? null;
1988
+ }
1989
+ async update(sid, data) {
1990
+ const k = this.key(sid);
1991
+ const patch = JSON.parse(JSON.stringify(data));
1992
+ const ttlSeconds = "expiresAt" in patch ? ttlSecondsFromExpiresAt(patch.expiresAt) : undefined;
1993
+ if (supportsRunScript(this.redis)) {
1994
+ for (let attempt = 0;attempt < SESSION_UPDATE_MAX_CAS_ATTEMPTS; attempt++) {
1995
+ const raw2 = await this.redis.get(k);
1996
+ if (raw2 == null)
1997
+ throw new Error(`Session with sid ${sid} not found`);
1998
+ const merged = mergeSessionRaw(raw2, patch);
1999
+ if (merged === undefined)
2000
+ throw new Error(`Session with sid ${sid} not found`);
2001
+ const result = await atomicCompareAndSet(this.redis, k, raw2, merged, ttlSeconds);
2002
+ if (result === "ok")
2003
+ return;
2004
+ if (result === "missing")
2005
+ throw new Error(`Session with sid ${sid} not found`);
2006
+ }
2007
+ }
2008
+ const raw = await this.redis.get(k);
2009
+ if (raw == null)
2010
+ throw new Error(`Session with sid ${sid} not found`);
2011
+ const session = reviveSession(raw, this.log);
2012
+ if (session == null)
2013
+ throw new Error(`Session with sid ${sid} not found`);
2014
+ const updated = { ...session, ...data };
2015
+ const value = serializeSession(updated);
2016
+ if (ttlSeconds != null && ttlSeconds > 0) {
2017
+ await this.redis.setex(k, Math.max(1, Math.floor(ttlSeconds)), value);
2018
+ } else {
2019
+ await this.redis.set(k, value);
2020
+ }
2021
+ }
2022
+ async delete(sid) {
2023
+ await this.redis.del(this.key(sid));
2024
+ }
2025
+ }
2026
+ // packages/redis-core/src/tenant-store.ts
2027
+ var IDS_SET3 = "tenant:ids";
2028
+ function serializeTenant(tenant) {
2029
+ return JSON.stringify({
2030
+ ...tenant,
2031
+ createdAt: tenant.createdAt.toISOString(),
2032
+ updatedAt: tenant.updatedAt.toISOString()
2033
+ });
2034
+ }
2035
+ function reviveTenant(data, log) {
2036
+ const raw = safeJsonParse(data, { store: "tenant", log });
2037
+ if (raw === undefined)
2038
+ return;
2039
+ return {
2040
+ ...raw,
2041
+ createdAt: new Date(raw.createdAt),
2042
+ updatedAt: new Date(raw.updatedAt)
2043
+ };
2044
+ }
2045
+
2046
+ class RedisTenantStoreData {
2047
+ redis;
2048
+ prefix;
2049
+ log;
2050
+ constructor(options) {
2051
+ this.redis = options.redis;
2052
+ this.prefix = getKeyPrefix(options, "tenant-store");
2053
+ this.log = getLogger(options);
2054
+ }
2055
+ key(...parts) {
2056
+ return prefixedKey(this.prefix, ...parts);
2057
+ }
2058
+ entityKey(id) {
2059
+ return this.key("tenant", encodeKeyId(id));
2060
+ }
2061
+ idsSetKey() {
2062
+ return this.key(IDS_SET3);
2063
+ }
2064
+ async get(id) {
2065
+ const raw = await this.redis.get(this.entityKey(id));
2066
+ if (raw == null)
2067
+ return;
2068
+ return reviveTenant(raw, this.log);
2069
+ }
2070
+ async upsert(tenant) {
2071
+ await new RedisTxn().set(this.entityKey(tenant.id), serializeTenant(tenant)).sadd(this.idsSetKey(), tenant.id).exec(this.redis);
2072
+ return tenant;
2073
+ }
2074
+ async list(options) {
2075
+ const ids = await this.redis.smembers(this.idsSetKey());
2076
+ if (ids.length === 0) {
2077
+ return applyListOptions([], options);
2078
+ }
2079
+ const keys = ids.map((id) => this.entityKey(id));
2080
+ const rawList = await this.redis.mget(keys);
2081
+ const items = rawList.map((raw) => raw != null ? reviveTenant(raw, this.log) : undefined).filter((t) => t != null);
2082
+ return applyListOptions(items, options);
2083
+ }
2084
+ async delete(id) {
2085
+ const existing = await this.get(id);
2086
+ await new RedisTxn().srem(this.idsSetKey(), id).del(this.entityKey(id)).exec(this.redis);
2087
+ return existing ? 1 : 0;
2088
+ }
2089
+ }
2090
+
2091
+ class RedisTenantStore {
2092
+ store;
2093
+ ionCache;
2094
+ constructor(options) {
2095
+ this.ionCache = options.ionCache;
2096
+ this.store = new RedisTenantStoreData(options);
2097
+ }
2098
+ async get(id) {
2099
+ return this.store.get(id);
2100
+ }
2101
+ async upsert(tenant) {
2102
+ const stored = await this.store.upsert(tenant);
2103
+ this.ionCache?.delete(tenant.id);
2104
+ return stored;
2105
+ }
2106
+ async list(options) {
2107
+ return this.store.list(options);
2108
+ }
2109
+ async delete(id) {
2110
+ const removed = await this.store.delete(id);
2111
+ this.ionCache?.delete(id);
2112
+ return removed;
2113
+ }
2114
+ async getIon(id) {
2115
+ if (!this.ionCache) {
2116
+ throw new Error(`${this.constructor.name}.getIon() requires an app-owned ionCache. Pass { ionCache } (an ` + `lru-cache instance whose fetchMethod builds the tenant Ionite and whose dispose ` + `closes it) — the SDK does not cache tenant runtimes. See the multi-tenant docs.`);
2117
+ }
2118
+ return this.ionCache.fetch(id);
2119
+ }
2120
+ }
2121
+ // packages/redis-core/src/user-store.ts
2122
+ import {
2123
+ list as list4,
2124
+ StoreConflictError as StoreConflictError4
2125
+ } from "@ionite/core";
2126
+ var IDS_SET4 = "user:ids";
2127
+ var NAME_INDEX_PREFIX2 = "user:name:";
2128
+ var EXTERNAL_ID_INDEX_PREFIX2 = "user:external-id:";
2129
+ function serializeUser(user) {
2130
+ const createdAt = user.createdAt ?? new Date;
2131
+ const updatedAt = user.updatedAt ?? createdAt;
2132
+ const obj = {
2133
+ ...user,
2134
+ createdAt: createdAt.toISOString(),
2135
+ updatedAt: updatedAt.toISOString()
2136
+ };
2137
+ if (obj.sso && typeof obj.sso === "object" && obj.sso !== null) {
2138
+ const sso = obj.sso;
2139
+ if (sso.expires instanceof Date)
2140
+ sso.expires = sso.expires.toISOString();
2141
+ }
2142
+ return JSON.stringify(obj);
2143
+ }
2144
+ function reviveUser(data, log) {
2145
+ const raw = safeJsonParse(data, { store: "user", log });
2146
+ if (raw === undefined)
2147
+ return;
2148
+ if (raw.sso && typeof raw.sso === "object" && raw.sso !== null) {
2149
+ const sso = raw.sso;
2150
+ if (typeof sso.expires === "string")
2151
+ sso.expires = new Date(sso.expires);
2152
+ }
2153
+ return {
2154
+ ...raw,
2155
+ createdAt: typeof raw.createdAt === "string" ? new Date(raw.createdAt) : undefined,
2156
+ updatedAt: typeof raw.updatedAt === "string" ? new Date(raw.updatedAt) : undefined
2157
+ };
2158
+ }
2159
+
2160
+ class RedisUserStore {
2161
+ redis;
2162
+ prefix;
2163
+ log;
2164
+ constructor(options) {
2165
+ this.redis = options.redis;
2166
+ this.prefix = getKeyPrefix(options, "user-store");
2167
+ this.log = getLogger(options);
2168
+ }
2169
+ key(...parts) {
2170
+ return prefixedKey(this.prefix, ...parts);
2171
+ }
2172
+ entityKey(id) {
2173
+ return this.key("user", encodeKeyId(id));
2174
+ }
2175
+ idsSetKey() {
2176
+ return this.key(IDS_SET4);
2177
+ }
2178
+ nameIndexKey(normalized) {
2179
+ return this.key(NAME_INDEX_PREFIX2 + encodeKeyId(normalized));
2180
+ }
2181
+ externalIdIndexKey(externalId) {
2182
+ return this.key(EXTERNAL_ID_INDEX_PREFIX2 + encodeKeyId(externalId));
2183
+ }
2184
+ async get(sub) {
2185
+ const raw = await this.redis.get(this.entityKey(sub));
2186
+ if (raw == null)
2187
+ return;
2188
+ return reviveUser(raw, this.log);
2189
+ }
2190
+ async getByExternalId(externalId) {
2191
+ const id = await this.redis.get(this.externalIdIndexKey(externalId));
2192
+ return id ? this.get(id) : undefined;
2193
+ }
2194
+ async lookup(user) {
2195
+ return this.get(user.id ?? "");
2196
+ }
2197
+ async upsert(user) {
2198
+ const userId = user.id?.trim() || crypto.randomUUID();
2199
+ const storedUser = { ...user, id: userId };
2200
+ const k = this.entityKey(userId);
2201
+ const existingRaw = await this.redis.get(k);
2202
+ const existing = existingRaw != null ? reviveUser(existingRaw, this.log) ?? null : null;
2203
+ const newName = storedUser.userName?.toLowerCase();
2204
+ const oldName = existing?.userName?.toLowerCase();
2205
+ const newExternalId = storedUser.externalId;
2206
+ const oldExternalId = existing?.externalId;
2207
+ const txn = new RedisTxn;
2208
+ if (newName)
2209
+ txn.claimUnique(this.nameIndexKey(newName), userId);
2210
+ if (newExternalId)
2211
+ txn.claimUnique(this.externalIdIndexKey(newExternalId), userId);
2212
+ if (oldName && oldName !== newName)
2213
+ txn.del(this.nameIndexKey(oldName));
2214
+ if (oldExternalId && oldExternalId !== newExternalId)
2215
+ txn.del(this.externalIdIndexKey(oldExternalId));
2216
+ txn.set(k, serializeUser(storedUser));
2217
+ if (newName)
2218
+ txn.set(this.nameIndexKey(newName), userId);
2219
+ if (newExternalId)
2220
+ txn.set(this.externalIdIndexKey(newExternalId), userId);
2221
+ txn.sadd(this.idsSetKey(), userId);
2222
+ if (await txn.exec(this.redis) === "conflict") {
2223
+ if (newExternalId) {
2224
+ const externalIdOwner = await this.redis.get(this.externalIdIndexKey(newExternalId));
2225
+ if (externalIdOwner && externalIdOwner !== userId) {
2226
+ throw new StoreConflictError4(`externalId "${newExternalId}" is already in use`, "unique_violation", "externalId");
2227
+ }
2228
+ }
2229
+ throw new StoreConflictError4(`userName "${storedUser.userName}" is already in use`, "unique_violation", "userName");
2230
+ }
2231
+ return storedUser;
2232
+ }
2233
+ async delete(sub) {
2234
+ const user = await this.get(sub);
2235
+ if (user == null)
2236
+ return 0;
2237
+ const txn = new RedisTxn;
2238
+ txn.del(this.entityKey(sub));
2239
+ if (user.userName)
2240
+ txn.del(this.nameIndexKey(user.userName.toLowerCase()));
2241
+ if (user.externalId)
2242
+ txn.del(this.externalIdIndexKey(user.externalId));
2243
+ txn.srem(this.idsSetKey(), sub);
2244
+ await txn.exec(this.redis);
2245
+ return 1;
2246
+ }
2247
+ async list(options) {
2248
+ const opts = normalizeListLimit(options);
2249
+ const ids = (await this.redis.smembers(this.idsSetKey())).sort();
2250
+ if (ids.length === 0)
2251
+ return list4(0, [], opts.start ?? 0, opts.limit);
2252
+ const keys = ids.map((id) => this.entityKey(id));
2253
+ const values = await this.redis.mget(keys);
2254
+ const arr = values.filter((v) => v != null).map((v) => reviveUser(v, this.log)).filter((u) => u != null);
2255
+ return applyListOptions(arr, opts);
2256
+ }
2257
+ }
2258
+ // packages/redis-core/src/workload-identity-store.ts
2259
+ import {
2260
+ list as list5,
2261
+ StoreConflictError as StoreConflictError5
2262
+ } from "@ionite/core";
2263
+ var IDS_SET5 = "workload-identity:ids";
2264
+ var CLIENT_INDEX_PREFIX = "workload-identity:client:";
2265
+ var EXTERNAL_ID_INDEX_PREFIX3 = "workload-identity:external:";
2266
+ function serializeIdentity(identity) {
2267
+ const createdAt = identity.createdAt ?? new Date;
2268
+ const updatedAt = identity.updatedAt ?? createdAt;
2269
+ return JSON.stringify({
2270
+ ...identity,
2271
+ createdAt: createdAt.toISOString(),
2272
+ updatedAt: updatedAt.toISOString()
2273
+ });
2274
+ }
2275
+ function reviveIdentity(data, log) {
2276
+ const raw = safeJsonParse(data, { store: "workload-identity", log });
2277
+ if (raw === undefined)
2278
+ return;
2279
+ return {
2280
+ ...raw,
2281
+ createdAt: typeof raw.createdAt === "string" ? new Date(raw.createdAt) : undefined,
2282
+ updatedAt: typeof raw.updatedAt === "string" ? new Date(raw.updatedAt) : undefined
2283
+ };
2284
+ }
2285
+
2286
+ class RedisWorkloadIdentityStore {
2287
+ redis;
2288
+ prefix;
2289
+ log;
2290
+ constructor(options) {
2291
+ this.redis = options.redis;
2292
+ this.prefix = getKeyPrefix(options, "workload-identity-store");
2293
+ this.log = getLogger(options);
2294
+ }
2295
+ key(...parts) {
2296
+ return prefixedKey(this.prefix, ...parts);
2297
+ }
2298
+ entityKey(id) {
2299
+ return this.key("workload-identity", encodeKeyId(id));
2300
+ }
2301
+ idsSetKey() {
2302
+ return this.key(IDS_SET5);
2303
+ }
2304
+ clientIndexKey(clientId) {
2305
+ return this.key(CLIENT_INDEX_PREFIX + encodeKeyId(clientId));
2306
+ }
2307
+ externalIdIndexKey(externalId) {
2308
+ return this.key(EXTERNAL_ID_INDEX_PREFIX3 + encodeKeyId(externalId));
2309
+ }
2310
+ async get(id) {
2311
+ const raw = await this.redis.get(this.entityKey(id));
2312
+ if (raw == null)
2313
+ return;
2314
+ return reviveIdentity(raw, this.log);
2315
+ }
2316
+ async lookup(identity) {
2317
+ if (identity.id) {
2318
+ const byId = await this.get(identity.id);
2319
+ if (byId)
2320
+ return byId;
2321
+ }
2322
+ if (identity.clientId) {
2323
+ const id = await this.redis.get(this.clientIndexKey(identity.clientId));
2324
+ if (id != null)
2325
+ return this.get(id);
2326
+ }
2327
+ if (identity.externalId) {
2328
+ return this.getByExternalId(identity.externalId);
2329
+ }
2330
+ return;
2331
+ }
2332
+ async getByExternalId(externalId) {
2333
+ const id = await this.redis.get(this.externalIdIndexKey(externalId));
2334
+ if (id == null)
2335
+ return;
2336
+ return this.get(id);
2337
+ }
2338
+ async upsert(identity) {
2339
+ const id = identity.id?.trim() || crypto.randomUUID();
2340
+ const k = this.entityKey(id);
2341
+ const existingRaw = await this.redis.get(k);
2342
+ let existing = null;
2343
+ if (existingRaw != null)
2344
+ existing = reviveIdentity(existingRaw, this.log) ?? null;
2345
+ const now = new Date;
2346
+ const stored = {
2347
+ ...identity,
2348
+ id,
2349
+ createdAt: existing?.createdAt ?? identity.createdAt ?? now,
2350
+ updatedAt: now
2351
+ };
2352
+ const txn = new RedisTxn;
2353
+ if (stored.clientId)
2354
+ txn.claimUnique(this.clientIndexKey(stored.clientId), id);
2355
+ if (stored.externalId)
2356
+ txn.claimUnique(this.externalIdIndexKey(stored.externalId), id);
2357
+ if (existing?.clientId && existing.clientId !== stored.clientId) {
2358
+ txn.del(this.clientIndexKey(existing.clientId));
2359
+ }
2360
+ if (existing?.externalId && existing.externalId !== stored.externalId) {
2361
+ txn.del(this.externalIdIndexKey(existing.externalId));
2362
+ }
2363
+ txn.set(k, serializeIdentity(stored));
2364
+ if (stored.clientId)
2365
+ txn.set(this.clientIndexKey(stored.clientId), id);
2366
+ if (stored.externalId)
2367
+ txn.set(this.externalIdIndexKey(stored.externalId), id);
2368
+ txn.sadd(this.idsSetKey(), id);
2369
+ if (await txn.exec(this.redis) === "conflict") {
2370
+ throw new StoreConflictError5(`clientId "${stored.clientId}" is already in use`, "unique_violation", "clientId");
2371
+ }
2372
+ return stored;
2373
+ }
2374
+ async delete(id) {
2375
+ const identity = await this.get(id);
2376
+ if (identity == null)
2377
+ return 0;
2378
+ const txn = new RedisTxn;
2379
+ txn.del(this.entityKey(id));
2380
+ if (identity.clientId)
2381
+ txn.del(this.clientIndexKey(identity.clientId));
2382
+ if (identity.externalId)
2383
+ txn.del(this.externalIdIndexKey(identity.externalId));
2384
+ txn.srem(this.idsSetKey(), id);
2385
+ await txn.exec(this.redis);
2386
+ return 1;
2387
+ }
2388
+ async list(options) {
2389
+ const opts = normalizeListLimit(options);
2390
+ const ids = (await this.redis.smembers(this.idsSetKey())).sort();
2391
+ if (ids.length === 0)
2392
+ return list5(0, [], opts.start ?? 0, opts.limit);
2393
+ const keys = ids.map((id) => this.entityKey(id));
2394
+ const values = await this.redis.mget(keys);
2395
+ const arr = values.filter((v) => v != null).map((v) => reviveIdentity(v, this.log)).filter((i) => i != null);
2396
+ return applyListOptions(arr, opts);
2397
+ }
2398
+ }
2399
+ // packages/redis-core/src/workload-token-store.ts
2400
+ function serializeToken(token) {
2401
+ return JSON.stringify({
2402
+ ...token,
2403
+ expires_at: token.expires_at.toISOString(),
2404
+ created_at: token.created_at.toISOString()
2405
+ });
2406
+ }
2407
+ function reviveToken(data, log) {
2408
+ const raw = safeJsonParse(data, { store: "workload-token", log });
2409
+ if (raw === undefined)
2410
+ return;
2411
+ return {
2412
+ ...raw,
2413
+ expires_at: new Date(raw.expires_at),
2414
+ created_at: new Date(raw.created_at)
2415
+ };
2416
+ }
2417
+
2418
+ class RedisWorkloadTokenStore {
2419
+ redis;
2420
+ prefix;
2421
+ log;
2422
+ constructor(options) {
2423
+ this.redis = options.redis;
2424
+ this.prefix = getKeyPrefix(options, "workload-token-store");
2425
+ this.log = getLogger(options);
2426
+ }
2427
+ key(workload_id) {
2428
+ return prefixedKey(this.prefix, "workload", "token", encodeKeyId(workload_id));
2429
+ }
2430
+ async set(token) {
2431
+ const k = this.key(token.workload_id);
2432
+ const ttlSec = Math.max(1, Math.floor((token.expires_at.getTime() - Date.now()) / 1000));
2433
+ await this.redis.setex(k, ttlSec, serializeToken(token));
2434
+ }
2435
+ async get(workload_id) {
2436
+ const raw = await this.redis.get(this.key(workload_id));
2437
+ if (raw == null)
2438
+ return null;
2439
+ const token = reviveToken(raw, this.log);
2440
+ if (token == null)
2441
+ return null;
2442
+ if (Date.now() > token.expires_at.getTime()) {
2443
+ await this.redis.del(this.key(workload_id));
2444
+ return null;
2445
+ }
2446
+ return token;
2447
+ }
2448
+ async delete(workload_id) {
2449
+ await this.redis.del(this.key(workload_id));
2450
+ }
2451
+ async isValid(workload_id) {
2452
+ const token = await this.get(workload_id);
2453
+ return token !== null;
2454
+ }
2455
+ async cleanup() {}
2456
+ }
2457
+ export {
2458
+ supportsRunScript,
2459
+ splitIonRedisSecret,
2460
+ scriptSha1,
2461
+ prefixedKey,
2462
+ getLogger,
2463
+ getKeyPrefix,
2464
+ encodeKeyId,
2465
+ createRedisLfvTransport,
2466
+ createReactiveRedis,
2467
+ createEvalshaRunner,
2468
+ chunkedMget,
2469
+ atomicCreate,
2470
+ atomicCompareAndSet,
2471
+ RedisWorkloadTokenStore,
2472
+ RedisWorkloadIdentityStore,
2473
+ RedisUserStore,
2474
+ RedisTxn,
2475
+ RedisTenantStore,
2476
+ RedisSessionStore,
2477
+ RedisResourceStore,
2478
+ RedisMagicLinkStore,
2479
+ RedisCustomerStore,
2480
+ RedisAccessIndex,
2481
+ RedisAccessCatalogStore,
2482
+ RedisAccessAssignmentStore,
2483
+ MAX_LIST_LIMIT,
2484
+ DEFAULT_MGET_CHUNK_SIZE,
2485
+ DEFAULT_LIST_LIMIT
2486
+ };