@ionite/redis-core 0.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,139 @@
1
+ # @ionite/redis-core
2
+
3
+ Redis-backed store implementations for ionite: **UserStore**, **SessionStore**, **MagicLinkStore**, **WorkloadTokenStore**, **AccessCatalogStore** (group/role/permission/resource definitions), **AccessAssignmentStore** (granted roots), **AccessIndex** (derived closure for fast `has*` checks), and **TenantStore**. Drop-in replacements for the in-memory stores from `@ionite/server`, suitable for production when you need persistence and/or multi-instance sharing.
4
+
5
+ This package provides the **stores**, the **RedisAdapter interface**, and the **driver-agnostic reactive helper** used by the package-specific `ionRedis(...)` wrappers. It does **not** include adapters for specific Redis clients.
6
+
7
+ ## When to use this package
8
+
9
+ - **Custom adapter:** You implement the `RedisAdapter` interface yourself (e.g. for Bun.redis, Upstash, or a custom wrapper). Install `@ionite/redis-core` and pass your adapter to the store constructors.
10
+ - **Custom reactive adapter:** You want the same secret-driven reconnect behavior as `@ionite/redis` / `@ionite/ioredis`, but for your own Redis client. Use `createReactiveRedis(...)` from this package.
11
+ - **Pre-built client:** Use one of the adapter packages instead so you get the adapter and stores from a single import. You do **not** need to install `@ionite/redis-core`:
12
+ - **node-redis (v5 or v4)** → `@ionite/redis` (includes adapter and stores)
13
+ - **ioredis** → `@ionite/ioredis`
14
+
15
+ ## Installation (custom adapter only)
16
+
17
+ ```bash
18
+ bun add @ionite/redis-core
19
+ ```
20
+
21
+ ## Adapter interface
22
+
23
+ Stores accept a **RedisAdapter**. Implement this interface for your Redis client:
24
+
25
+ - **Required:** `get`, `set`, `setex`, `del`, `sadd`, `srem`, `smembers`, `mget`
26
+ - **List ordering:** `list()` returns records in a **stable, deterministic order** (ids sorted lexicographically via `SMEMBERS` + `MGET`), so offset pagination is coherent across pages. No sorted-set support is required; UI-style sorting is the caller's concern.
27
+ - **Optional (atomic consume):** `getdel(key)` — when present, the magic-link store consumes a link atomically (Redis `GETDEL`, **Redis 6.2+**) so concurrent consumes yield exactly one winner; otherwise it falls back to a best-effort get-then-del. The bundled `@ionite/redis` and `@ionite/ioredis` adapters implement it natively, and `ionRedis(...)` forwards it from the active client.
28
+ - **Optional (intra-store atomicity):** `runScript(script, keys, args)` — runs a server-side Lua script (the Redis `EVAL` command; **not** the JavaScript `eval()` sink — nothing is dynamically executed in your process). When present, the stores run create-if-absent, unique-index claim-or-fail, multi-key writes, and session compare-and-set read-modify-write as one all-or-nothing unit (§5.9, §8.2). When absent, the stores fall back to best-effort sequenced writes that still perform all checks before any write (fail-fast + idempotent replay); only write isolation differs. A genuine create/uniqueness collision throws a typed `StoreConflictError` (from `@ionite/core`); an identical replay is treated as success. Script bodies are static SDK-authored text and all dynamic values are passed via `keys`/`args` (referenced as `KEYS[i]`/`ARGV[j]`), so there is no Lua-injection vector. `ionRedis(...)` forwards `runScript` from the active client, so the atomic paths apply over secret-backed reactive connections too.
29
+ - **Session updates (§8.2):** `SessionStore.update(...)` reads the record, shallow-merges the patch in JS (exact `JSON.stringify` semantics — arrays and large values of untouched fields are left byte-for-byte, no `cjson` round-trip), then compare-and-sets, retrying on contention before a final best-effort write. Concurrent partial updates cannot clobber each other. The session key TTL is derived from `session.expiresAt`: `create` uses `SET ... EX`, and `update`'s compare-and-set carries `EX` (or `KEEPTTL` when the merged record has no `expiresAt`), so expired and refresh-rotation-grace records are physically reaped by Redis with no read-path bookkeeping. The optional adapter `ttl(key)` method exposes the remaining TTL for diagnostics/validation.
30
+
31
+ ## Reactive helper
32
+
33
+ `createReactiveRedis(...)` creates one stable `RedisAdapter` object that can swap its underlying connected client when a secret version changes. The package-specific `ionRedis(...)` wrappers use this helper internally.
34
+
35
+ Use it when:
36
+
37
+ - your Redis client is not node-redis or ioredis
38
+ - the Redis connection details come from `ion.secrets`
39
+ - you want one stable adapter that can be constructed before secrets arrive
40
+
41
+ Your custom client factory returns `{ adapter, dispose? }`, and the helper handles source-bound async get-then-watch secret subscription, reconnect dedupe by secret version, source-handle swaps during a config change, operation-level waiting until the first client is ready, and `ready()` when you want an explicit startup barrier. Subscriptions default to fail-fast, so missing Redis secrets reject `ready()` and store operations instead of waiting silently forever; pass `subscription: { mode: 'wait', timeout?: number }` only when waiting for the secret to appear is intentional.
42
+
43
+ ## Options
44
+
45
+ All store constructors take **RedisStoreOptions**:
46
+
47
+ - **`redis`** (required): A `RedisAdapter` instance.
48
+ - **`keyPrefix`** (**required**): Key prefix for all keys. There is no implicit default. In multi-tenant deployments each tenant MUST use a distinct prefix (e.g. `es:t:<tenantId>:`) so the global index sets (`user:ids`, `tenant:ids`, …) auto-namespace and never collide across tenants. An empty or missing prefix throws.
49
+ - **`logger`** (optional): `Logger` from `@ionite/core` (default `defaultLogger`).
50
+
51
+ `RedisTenantStore` also accepts:
52
+
53
+ - **`ionCache`** (optional): an app-owned `TenantIonCache` (`{ fetch(id), delete(id) }`, structurally compatible with `LRUCache<string, Ionite>`). `getIon(id)` delegates to `ionCache.fetch(id)`; calling `getIon(...)` without an `ionCache` throws. `upsert`/`delete` call `ionCache.delete(id)` so a changed/removed record invalidates (and, via the cache's `dispose`, closes) the stale runtime. The SDK never caches tenant runtimes itself — the tenant *record* is a plain, non-sensitive Redis lookup; only the runtime (which reads sensitive RemoteConfig from the vault) is cached, and that cache is yours.
54
+ - **Tenant config persistence:** Redis tenant stores persist tenant metadata, including an optional narrow `TenantConfigLocator` (`{ source?, path, ttl? }`) stored at `tenant.configLocator`. In your cache's `fetchMethod`, call `tenantConfigSource(tenant.configLocator, es)` (passing the root `es`) to materialize the runtime `ConfigSource` when a locator is present, and wire `dispose: (es) => ion.close()` so evicted runtimes are released. See the "Tenant runtime cache" section of the root README for the canonical `lru-cache` pattern.
55
+ - **Cross-node staleness:** each node's `ionCache` is independent, so a record change on one node is visible on another only when that node's cache entry expires (TTL) or it mutates the tenant itself. Size the cache `ttl` to your acceptable record-staleness window; config *content* changes still propagate live via the ConfigSource subscription.
56
+ - **Directory note:** the SDK no longer ships tenant-directory methods (`findTenants`/`findUserTenants`) or `/mine`/`/discovery` endpoints. If your app needs a "which tenants can this user switch to" surface, build it in app code (e.g. a SQL join plus the kept `discoverSessionRecords`) with your own auth and rate limiting.
57
+
58
+ ## Example (custom adapter)
59
+
60
+ ```ts
61
+ import type { RedisAdapter } from '@ionite/redis-core';
62
+ import {
63
+ RedisAccessIndex,
64
+ RedisAccessAssignmentStore,
65
+ RedisAccessCatalogStore,
66
+ RedisUserStore,
67
+ RedisSessionStore,
68
+ } from '@ionite/redis-core';
69
+
70
+ const myAdapter: RedisAdapter = {
71
+ async get(key) { /* ... */ return null; },
72
+ async set(key, value) { /* ... */ },
73
+ async setex(key, seconds, value) { /* ... */ },
74
+ async del(key) { /* ... */ },
75
+ async sadd(key, ...members) { /* ... */ },
76
+ async srem(key, ...members) { /* ... */ },
77
+ async smembers(key) { /* ... */ return []; },
78
+ async mget(keys) { /* ... */ return keys.map(() => null); },
79
+ };
80
+
81
+ const keyPrefix = 'es:t:acme:'; // required, distinct per tenant
82
+ const userStore = new RedisUserStore({ redis: myAdapter, keyPrefix });
83
+ const sessionStore = new RedisSessionStore({ redis: myAdapter, keyPrefix });
84
+ const accessStore = new RedisAccessAssignmentStore({ redis: myAdapter, keyPrefix });
85
+ const accessIndex = new RedisAccessIndex({ redis: myAdapter, keyPrefix });
86
+ const catalogStore = new RedisAccessCatalogStore({ redis: myAdapter, keyPrefix });
87
+ ```
88
+
89
+ ## Key layout
90
+
91
+ Keys are prefixed with the required `keyPrefix` (examples below use `es:`):
92
+
93
+ - **User:** `es:user:{id}`, indexes, set `es:user:ids`.
94
+ - **Session:** `es:session:{sid}` (with TTL derived from `session.expiresAt`). Opaque auth sessions are looked up by `sid` alone within a configured `SessionStore`; tenant identity and claims are stored in the session value, not in a tenant-specific session key namespace.
95
+ - **Magic link:** `es:magic:{token}` (with TTL).
96
+ - **Workload token:** `es:workload:token:{workload_id}` (with TTL).
97
+ - **Group:** `es:group:{id}`, indexes, set `es:group:ids`.
98
+ - **Authz Catalog:** `es:authz:{group|role|permission}:{id}` plus per-kind `ids` sets. `RedisAccessCatalogStore` stores SCIM/IAM-sourced groups, roles, and permissions for SCIM Groups aggregation and OAA full sync (code-authored definitions belong in the code-declared catalog — `defineCatalog(...)` -> `FrameworkConfig.authz.catalog` — and are never seeded here). `set(entity)` returns a ready-aware catalog entry synchronously and performs the Redis write in the background; `entry.ready()` resolves once the write lands (and re-surfaces a failed write) while `entry.isReady()` reports its status.
99
+ - **Resources:** `es:authz:resource:{id}` plus an `ids` set. `RedisResourceStore` is a Redis-backed writable `ResourceProvider` (`FrameworkStores.resources`) for apps without their own resource tables: `set(resource)` / `delete(id)` persist and emit the `changed` signal, and the optional `deriveEntitlements`/`resolveEntitlement` pair derives requestable per-resource entitlement Groups on demand.
100
+ - **Access roots:** `es:access:{id}` (id = `accessRootId`, scope-aware), subject/target indexes, set `es:access:ids`. `RedisAccessAssignmentStore` stores granted `AccessChange` roots (with provenance, status, and resource scope) for SCIM grant/revoke routing and OAA full sync.
101
+ - **Access index (derived closure):** `es:idx:*` presence keys (`idx:has`, `idx:any`) for `MGET` checks, edge-roots sets (`idx:er`), triple sets (`idx:tr`), per-root/per-subject descriptor sets (`idx:root`, `idx:subj`), and a subject-disabled marker (`idx:disabled:{subject}`) that makes every `has*` deny with `metadata.reason: 'subject_disabled'` while it is set (independent of edge materialization, so `clear`/reconcile never wipes it). `RedisAccessIndex` stores root-attributed effective edges so `removeByRoot` is efficient and diamond grants survive partial revokes.
102
+ - **Tenant:** `es:tenant:{id}` (record, id encoded per §10.2) plus the `es:tenant:ids` index set. There is no separate company index; `companyId` is a field on the tenant record, not a key namespace.
103
+
104
+ ## Key encoding (external ids)
105
+
106
+ Externally-sourced id segments (SCIM `id`/`externalId`, `userName`, subject/target/resource ids, client ids, magic-link tokens, session ids, the IAM-delta id-map parts) are **encoded** before they are joined into a key (§10.2), so an id can never split a key on a stray `:` or collide with a structural index segment (a user whose id is literally `ids` cannot clobber the `user:ids` set). SDK-authored literal segments (`idx:has`, `user:ids`, `authz`, …) stay raw. The encoding is total, injective, and length-bounded:
107
+
108
+ - **Safe, short ids** (`^[A-Za-z0-9_.@|-]{1,64}$`) → `=<id>` verbatim (readable: `es:user:=alice`).
109
+ - **Everything else** (any `:`/`#`/control char, or length > 64) → `#<sha256(id) truncated to 128 bits, base64url>` (fixed length, collision-resistant).
110
+
111
+ `#` and `=` never appear in a structural segment and neither branch emits a `:`, so the data and structural key namespaces are provably disjoint. `encodeKeyId(id)` is exported for Ops debugging (compute the stored key form of an id). This is a **greenfield** scheme with no migration from any prior key layout.
112
+
113
+ ## Redis access checks
114
+
115
+ Fast authorization checks read the **`RedisAccessIndex`** (the derived closure), not the `RedisAccessAssignmentStore` roots. The index is materialized from roots + the catalog by the SDK SCIM orchestration (or `expandRoot` + `materialize`), so unlike a raw edge store these checks already account for group -> role -> permission expansion, nested groups/roles, and `applyToSubResources` sub-resource inheritance:
116
+
117
+ - **`RedisAccessAssignmentStore`** — direct grants only (for example `alice → role:editor` and `alice → role:reviewer` are two roots).
118
+ - **`RedisAccessIndex`** — effective access after expansion (both roles **and** every permission they confer; overlapping permissions such as `read` from both roles are stored with separate `rootId` attribution so revoking one role does not remove access still justified by the other).
119
+
120
+ ```ts
121
+ await accessIndex.has(userId, 'permission', permissionId);
122
+ await accessIndex.hasGroup(userId, groupId);
123
+ await accessIndex.hasRole(userId, roleId);
124
+ await accessIndex.hasPermission(userId, permissionId, resourceId /* optional scope */);
125
+ await accessIndex.hasAny(userId, [
126
+ { targetType: 'role', target: 'admin' },
127
+ { targetType: 'permission', target: 'invoice:read' },
128
+ ]);
129
+ await accessIndex.hasAll(userId, [
130
+ { targetType: 'role', target: 'finance' },
131
+ { targetType: 'permission', target: 'invoice:approve' },
132
+ ]);
133
+ ```
134
+
135
+ Single checks read presence keys directly; resource-scoped checks also accept an application-wide grant. Bounded `hasAny(...)` and `hasAll(...)` issue per-target reads and log a warning when the requested target set is large. All checks return the shared `AuthzDecision` (`{ allow, matched? }`).
136
+
137
+ ## List ordering
138
+
139
+ `list()` returns records in a **stable, deterministic order**: ids are read via `SMEMBERS` and sorted lexicographically before `MGET`, so offset pages (`start`/`limit`) are disjoint and complete across calls for an unchanged dataset. There is no caller-specified sort and no sorted-set requirement; UI-style sorting is the application's concern.
@@ -0,0 +1,507 @@
1
+ import { AccessAssignmentStore, AccessChange, ListOptions, ListResult } from "@ionite/core";
2
+ import { Logger } from "@ionite/core";
3
+ /**
4
+ * Minimal Redis adapter interface for @ionite/redis-core stores.
5
+ *
6
+ * Implement this interface to use ioredis, node-redis, or a custom Redis wrapper.
7
+ * `list()` returns records in a stable (lexicographic id) order via SMEMBERS + MGET; no sorted-set
8
+ * support is required.
9
+ */
10
+ /**
11
+ * Required Redis operations used by all stores.
12
+ */
13
+ interface RedisAdapter {
14
+ get(key: string): Promise<string | null>;
15
+ set(key: string, value: string): Promise<void>;
16
+ setex(key: string, seconds: number, value: string): Promise<void>;
17
+ del(key: string): Promise<void>;
18
+ /**
19
+ * Optional: return a key's remaining TTL in seconds (Redis `TTL`): `-1` = no expiry, `-2` = the key
20
+ * does not exist. Purely diagnostic for the SDK (session TTLs are set via `setex`/`SET EX`); exposed
21
+ * so validation/tests can assert a session record carries a bounded TTL.
22
+ */
23
+ ttl?(key: string): Promise<number>;
24
+ sadd(key: string, ...members: string[]): Promise<void>;
25
+ srem(key: string, ...members: string[]): Promise<void>;
26
+ smembers(key: string): Promise<string[]>;
27
+ mget(keys: string[]): Promise<(string | null)[]>;
28
+ /**
29
+ * Optional: atomically get a key's value and delete it (Redis `GETDEL`, 6.2+). When implemented,
30
+ * the magic-link store uses it for a truly atomic single-use consume; otherwise it falls back to
31
+ * a best-effort get-then-del.
32
+ */
33
+ getdel?(key: string): Promise<string | null>;
34
+ /**
35
+ * Optional: run a server-side Lua script atomically (the Redis `EVAL` command — this is NOT the
36
+ * JavaScript `eval()` sink; nothing in our process is dynamically executed). When implemented, the
37
+ * stores use it for intra-store atomicity (§5.9) — create-if-absent, unique-index claim-or-fail,
38
+ * multi-key writes as one all-or-nothing unit, and session read-modify-write — so a single op never
39
+ * half-lands. Scripts are written checks-first: a failed check returns before any write, so a
40
+ * conflict leaves zero writes. When this is absent the stores fall back to best-effort sequenced
41
+ * writes + fail-fast and rely on the caller replaying the (idempotent) push; correctness is
42
+ * preserved, but the writes are not isolated.
43
+ *
44
+ * SECURITY: script bodies are static SDK-authored text; every dynamic value (keys, members, scores,
45
+ * serialized records, TTLs) is passed out-of-band via `keys`/`args` and referenced as
46
+ * `KEYS[i]`/`ARGV[j]` — data is never concatenated into the script source, so there is no Lua
47
+ * injection vector (the same posture as parameterized SQL).
48
+ *
49
+ * @param script Lua source (static, SDK-authored).
50
+ * @param keys `KEYS[1..n]`.
51
+ * @param args `ARGV[1..n]`.
52
+ * @returns the script's reply (commonly a status string the caller normalizes).
53
+ */
54
+ runScript?(script: string, keys: string[], args: string[]): Promise<unknown>;
55
+ }
56
+ /**
57
+ * Type guard: adapter can run server-side Lua scripts (Redis `EVAL`) for intra-store atomicity (§5.9).
58
+ */
59
+ declare function supportsRunScript(adapter: RedisAdapter): adapter is RedisAdapter & {
60
+ runScript: NonNullable<RedisAdapter["runScript"]>;
61
+ };
62
+ /**
63
+ * Shared options for all Redis store constructors.
64
+ */
65
+ interface RedisStoreOptions {
66
+ /** Redis adapter (from createIoredisAdapter, createRedisAdapter, or custom). */
67
+ redis: RedisAdapter;
68
+ /**
69
+ * Key prefix for all keys. REQUIRED: there is no implicit default. In multi-tenant deployments
70
+ * each tenant MUST use a distinct prefix (e.g. `es:t:<tenantId>:`) so the global index sets
71
+ * (`user:ids`, `tenant:ids`, etc.) auto-namespace and never collide across tenants.
72
+ */
73
+ keyPrefix: string;
74
+ /** Logger for version warnings (default defaultLogger from core). */
75
+ logger?: Logger;
76
+ }
77
+ /**
78
+ * Validate the required `keyPrefix` and warn (at most once per fingerprint) when the same
79
+ * `(storeKind, keyPrefix)` is constructed more than once in a process. A duplicate almost always
80
+ * means two tenants share a Redis namespace; distinct tenants MUST use distinct prefixes (e.g.
81
+ * `es:t:<tenantId>:`). This is a warning, not a throw, because legitimate re-initialization (same
82
+ * tenant, same process — notably an LRU evict→recreate cycle) can reconstruct a store; the once-per
83
+ * -fingerprint dedup keeps that from becoming recurring false-positive noise while still surfacing an
84
+ * accidental shared keyspace loudly the first time.
85
+ */
86
+ declare function getKeyPrefix(options: RedisStoreOptions, storeKind?: string): string;
87
+ declare function getLogger(options: RedisStoreOptions): Logger;
88
+ declare function prefixedKey(prefix: string, ...parts: string[]): string;
89
+ /**
90
+ * Encode an externally-sourced id into a collision-free key segment (AUDIT_HISTORY.md v2 §10.2).
91
+ *
92
+ * External ids (SCIM `externalId`, `userName`, subject/target/resource ids, client ids) are joined
93
+ * into Redis keys with `:`. Left raw, an id containing `:` could shift key segments and read/write
94
+ * another principal's slot, and an id that equals a structural token (e.g. a user whose id is
95
+ * `ids`) could clobber an index key (`user:ids`). Both are privilege/integrity bugs, not cosmetic.
96
+ *
97
+ * The encoding is total and injective, and every output carries a leading sentinel that structural
98
+ * tokens never use, so the "data" and "structural" key namespaces are provably disjoint:
99
+ * - safe, short ids -> `=<id>` verbatim (readable for the common case: uuids, ulids, numeric ids);
100
+ * - everything else (any `:`/`#`/control char, or length > 64) -> `#<sha256(id) truncated to
101
+ * 128 bits, base64url>` — fixed length (bounded keys) and collision-resistant.
102
+ *
103
+ * `#` and `=` never appear in a structural segment, and neither branch can emit a `:`, so a data
104
+ * segment can neither impersonate a structural token nor split the key. Exported so Ops can compute
105
+ * the key form of an id when debugging.
106
+ */
107
+ declare function encodeKeyId(id: string): string;
108
+ /**
109
+ * Redis-backed store of granted access **roots** (source of truth). Roots are keyed by their
110
+ * deterministic id (`accessRootId`, scope-aware) and carry provenance + status. Fast `has*`
111
+ * checks live on the derived `RedisAccessIndex`, not here.
112
+ */
113
+ declare class RedisAccessAssignmentStore implements AccessAssignmentStore<AccessChange> {
114
+ private readonly redis;
115
+ private readonly prefix;
116
+ private readonly log;
117
+ constructor(options: RedisStoreOptions);
118
+ private key;
119
+ private idsSetKey;
120
+ private accessStorageKey;
121
+ private subjectIndexKey;
122
+ private targetIndexKey;
123
+ grant(change: AccessChange): Promise<void>;
124
+ revoke(change: AccessChange): Promise<void>;
125
+ get(id: string): Promise<AccessChange | undefined>;
126
+ list(options?: ListOptions): Promise<ListResult<AccessChange>>;
127
+ listByTarget(targetType: AccessChange["targetType"], target: string, options?: ListOptions): Promise<ListResult<AccessChange>>;
128
+ listBySubject(subject: string, subjectType?: AccessChange["subjectType"], options?: ListOptions): Promise<ListResult<AccessChange>>;
129
+ private listInMemory;
130
+ private listFromIds;
131
+ }
132
+ import { AccessCatalogStore, CatalogChangeListener, EntityStore, Group, ListOptions as ListOptions2, ListResult as ListResult2, Permission, Resource, ResourceChangeListener, ResourceProvider, Role } from "@ionite/core";
133
+ declare class RedisAccessCatalogStore implements AccessCatalogStore {
134
+ readonly groups: EntityStore<Group>;
135
+ readonly roles: EntityStore<Role>;
136
+ readonly permissions: EntityStore<Permission>;
137
+ private readonly listeners;
138
+ constructor(options: RedisStoreOptions);
139
+ afterChange(listener: CatalogChangeListener): () => void;
140
+ }
141
+ /**
142
+ * Writable, Redis-backed {@link ResourceProvider} for apps that have no resource table of their own
143
+ * (the common case is implementing `ResourceProvider` directly over the app's database — resources
144
+ * already live there; use this store only when Redis IS the app's resource storage).
145
+ *
146
+ * `set`/`delete` emit the `changed` signal after the durable write lands, driving the SDK's
147
+ * adjacency-cache invalidation, targeted reconcile, and incremental OAA projection. Provide
148
+ * `deriveEntitlements`/`resolveEntitlement` to expose resource-derived entitlement Groups.
149
+ */
150
+ declare class RedisResourceStore implements ResourceProvider {
151
+ private readonly redis;
152
+ private readonly prefix;
153
+ private readonly log;
154
+ private readonly listeners;
155
+ entitlements?: (resource: Resource) => Group[];
156
+ entitlement?: (id: string) => Promise<Group | undefined>;
157
+ constructor(options: RedisStoreOptions & {
158
+ /** Pure derivation of a resource's requestable entitlement Groups (see {@link ResourceProvider.entitlements}). */
159
+ deriveEntitlements?: (resource: Resource) => Group[];
160
+ /** Reverse resolution of a resource-derived entitlement id (see {@link ResourceProvider.entitlement}). */
161
+ resolveEntitlement?: (id: string, get: (resourceId: string) => Promise<Resource | undefined>) => Promise<Group | undefined>;
162
+ });
163
+ private key;
164
+ private entityKey;
165
+ private idsSetKey;
166
+ get(id: string): Promise<Resource | undefined>;
167
+ list(options?: ListOptions2): Promise<ListResult2<Resource>>;
168
+ children(id: string): Promise<Resource[]>;
169
+ changed(listener: ResourceChangeListener): () => void;
170
+ /** Upsert a resource; resolves after the durable write lands and the `changed` signal is emitted. */
171
+ set(resource: Resource): Promise<void>;
172
+ /** Delete a resource; resolves after the durable delete lands and the `changed` signal is emitted. */
173
+ delete(id: string): Promise<void>;
174
+ private emitChange;
175
+ }
176
+ import { AccessCheckTarget, AccessIndex, AccessRef, AuthzDecision, BaseUser, EffectiveEdge, Group as Group2, OaaAccessTargetType, Permission as Permission2, Resource as Resource2, Role as Role2, SubjectRef, SubjectType } from "@ionite/core";
177
+ /**
178
+ * Redis-backed derived closure index. Edges are root-attributed: an edge-roots SET tracks which
179
+ * roots justify each `(subject, targetType, target, resource)` edge, so `removeByRoot` only drops
180
+ * an edge once no roots remain. Existence checks read presence keys via `MGET`.
181
+ *
182
+ * Rebuildable from AccessAssignmentStore roots + the catalog.
183
+ */
184
+ declare class RedisAccessIndex implements AccessIndex {
185
+ private readonly redis;
186
+ private readonly prefix;
187
+ private readonly log;
188
+ constructor(options: RedisStoreOptions);
189
+ private key;
190
+ private subjectKeyPart;
191
+ private hasKey;
192
+ private anyKey;
193
+ private edgeRootsKey;
194
+ private tripleKey;
195
+ private rootKey;
196
+ private subjectKey;
197
+ private subjectsKey;
198
+ /**
199
+ * Subject-disabled marker (plain presence key so it batches into the existing `MGET` reads).
200
+ * Deliberately independent of edge materialization: `materialize`/`removeByRoot`/`clear` and
201
+ * reconcile never touch it, so a rebuild cannot silently re-enable a SCIM-deactivated subject. The
202
+ * subject segment is encoded (§10.2) so the Batch 1 veto can never be aliased or evaded via a
203
+ * crafted subject id.
204
+ */
205
+ private disabledKey;
206
+ has(subject: AccessRef<BaseUser>, targetType: OaaAccessTargetType, target: AccessRef<Group2 | Role2 | Permission2>, resource?: AccessRef<Resource2>, subjectType?: SubjectType): Promise<AuthzDecision>;
207
+ hasGroup(subject: AccessRef<BaseUser>, group: AccessRef<Group2>, resource?: AccessRef<Resource2>, subjectType?: SubjectType): Promise<AuthzDecision>;
208
+ hasRole(subject: AccessRef<BaseUser>, role: AccessRef<Role2>, resource?: AccessRef<Resource2>, subjectType?: SubjectType): Promise<AuthzDecision>;
209
+ hasPermission(subject: AccessRef<BaseUser>, permission: AccessRef<Permission2>, resource?: AccessRef<Resource2>, subjectType?: SubjectType): Promise<AuthzDecision>;
210
+ hasAny(subject: AccessRef<BaseUser>, targets: AccessCheckTarget[], subjectType?: SubjectType): Promise<AuthzDecision>;
211
+ hasAll(subject: AccessRef<BaseUser>, targets: AccessCheckTarget[], subjectType?: SubjectType): Promise<AuthzDecision>;
212
+ /**
213
+ * Evaluate many targets for one subject with a single batched `MGET` (§7.4) instead of a per-target
214
+ * round trip. Each target contributes 1 (resource-agnostic) or 2 (resource + app-wide) presence
215
+ * keys to one flat read; decisions are reconstructed in the original order.
216
+ */
217
+ private evaluateTargets;
218
+ /** The five derived keys for an edge, in the fixed [edgeRoots, has, triple, any, subject] order. */
219
+ private edgeKeys;
220
+ /**
221
+ * Re-materialize a root's closure as a DIFF against its currently-stored descriptors: only edges
222
+ * that appear/disappear are written/torn down, so unchanged edges are never touched (closes the
223
+ * §5.3 deny window and the teardown-vs-materialize race). One atomic script when `runScript` is
224
+ * available; otherwise a sequential fallback applying the same diff (documented: not isolated).
225
+ */
226
+ materialize(rootId: string, edges: EffectiveEdge[]): Promise<void>;
227
+ removeByRoot(rootId: string): Promise<void>;
228
+ listSubjects(): Promise<SubjectRef[]>;
229
+ setSubjectDisabled(subject: string, subjectType: SubjectType, disabled: boolean): Promise<void>;
230
+ isSubjectDisabled(subject: string, subjectType?: SubjectType): Promise<boolean>;
231
+ clear(subject?: string, subjectType?: SubjectType): Promise<void>;
232
+ private clearSubject;
233
+ private dropRootFromEdge;
234
+ private allow;
235
+ private warnLargeCheck;
236
+ }
237
+ /**
238
+ * Default MGET batch size (§7.3). Large id sets must not be loaded as one giant `MGET` command —
239
+ * adapters chunk the keys so a single round trip can't build an unbounded command/response.
240
+ */
241
+ declare const DEFAULT_MGET_CHUNK_SIZE = 512;
242
+ /**
243
+ * Fetch many keys via a chunked `MGET`, preserving input order and normalizing missing values to
244
+ * `null`. `fetch` is the adapter's native multi-get over a bounded slice of keys.
245
+ */
246
+ declare function chunkedMget(keys: string[], fetch: (chunk: string[]) => Promise<(string | null | undefined)[]>, chunkSize?: number): Promise<(string | null)[]>;
247
+ /** Result of an atomic create-if-absent. */
248
+ type AtomicCreateResult = "created" | "exists_identical" | "conflict";
249
+ /**
250
+ * Atomically create a key only if absent. A byte-identical existing value reads back as
251
+ * `exists_identical` (replay-safe); a different value is a `conflict` (fail-fast). With `ttlSeconds`
252
+ * the write carries an expiry (`SET ... EX`).
253
+ */
254
+ declare function atomicCreate(adapter: RedisAdapter, key: string, value: string, opts?: {
255
+ ttlSeconds?: number;
256
+ }): Promise<AtomicCreateResult>;
257
+ /** Result of an atomic compare-and-set (§8.2). */
258
+ type AtomicCompareAndSetResult = "ok" | "missing" | "retry";
259
+ /**
260
+ * Atomic compare-and-set (§8.2). Writes `newRaw` only when the key currently holds exactly
261
+ * `expectedRaw` (byte comparison); returns `'missing'` when absent and `'retry'` when the stored
262
+ * value has changed since the caller read it. The read-modify itself happens in the caller (a plain
263
+ * JS merge with exact `JSON.stringify` semantics), so this Lua does no `cjson` decode/encode — the
264
+ * old `cjson` round-trip silently corrupted sessions (empty arrays became `{}`, large integers lost
265
+ * precision). Returns `'unsupported'` when the adapter has no `runScript` so the caller can fall back
266
+ * to a best-effort get-modify-set.
267
+ *
268
+ * `newTtlSeconds` controls the key's expiry on the write (AUDIT_HISTORY.md §1.3): a positive value writes with
269
+ * `EX` so a refreshed session record keeps a bounded TTL; when omitted the existing TTL is preserved
270
+ * (`KEEPTTL`) so an incidental update never accidentally makes a TTL'd key immortal.
271
+ */
272
+ declare function atomicCompareAndSet(adapter: RedisAdapter, key: string, expectedRaw: string, newRaw: string, newTtlSeconds?: number): Promise<AtomicCompareAndSetResult | "unsupported">;
273
+ /**
274
+ * Builds an ordered set of unique-index claims (checks) + writes and executes them as one unit:
275
+ * a single Lua script (checks-first) when the adapter supports `EVAL`, else a best-effort sequenced
276
+ * fallback that still performs all checks before any write. `exec` returns `'conflict'` (and writes
277
+ * nothing) when any claimed unique index is already owned by a different id.
278
+ */
279
+ declare class RedisTxn {
280
+ private readonly claims;
281
+ private readonly writes;
282
+ /** Claim a unique secondary index: conflict unless `indexKey` is absent or already == `ownerId`. */
283
+ claimUnique(indexKey: string, ownerId: string): this;
284
+ set(key: string, value: string): this;
285
+ setex(key: string, ttl: number, value: string): this;
286
+ del(key: string): this;
287
+ sadd(key: string, ...members: string[]): this;
288
+ srem(key: string, ...members: string[]): this;
289
+ exec(adapter: RedisAdapter): Promise<"ok" | "conflict">;
290
+ private execLua;
291
+ private execSequential;
292
+ }
293
+ import { BaseCustomer, CustomerStore, ListOptions as ListOptions3, ListResult as ListResult3 } from "@ionite/core";
294
+ type RedisCustomerStoreOptions = RedisStoreOptions;
295
+ declare class RedisCustomerStore<TExtended = Record<string, never>> implements CustomerStore<TExtended> {
296
+ private readonly redis;
297
+ private readonly prefix;
298
+ private readonly log;
299
+ constructor(options: RedisCustomerStoreOptions);
300
+ private key;
301
+ private entityKey;
302
+ private idsSetKey;
303
+ private nameIndexKey;
304
+ get(sub: string): Promise<BaseCustomer<TExtended> | undefined>;
305
+ lookup(customer: BaseCustomer<TExtended>): Promise<BaseCustomer<TExtended> | undefined>;
306
+ upsert(customer: BaseCustomer<TExtended>): Promise<BaseCustomer<TExtended>>;
307
+ delete(sub: string): Promise<number>;
308
+ list(options?: ListOptions3): Promise<ListResult3<BaseCustomer<TExtended>>>;
309
+ }
310
+ /** SHA1 of a Lua script body — the id Redis caches scripts under for `EVALSHA`. */
311
+ declare function scriptSha1(script: string): string;
312
+ /**
313
+ * Wrap a client's `EVAL`/`EVALSHA` into a `runScript` that memoizes each unique script body's SHA1
314
+ * and prefers `EVALSHA`, falling back to `EVAL` once on `NOSCRIPT` (the server evicted/never cached
315
+ * the script) and priming the cache for next time. Because every SDK script body is static text, the
316
+ * memo stays tiny and steady-state calls ship only the 40-byte sha, not the full source — this is what
317
+ * keeps a hot grant/materialize path from re-transmitting (and the server from re-compiling) the same
318
+ * script on every request.
319
+ */
320
+ declare function createEvalshaRunner(client: {
321
+ eval(script: string, keys: string[], args: string[]): Promise<unknown>;
322
+ evalsha(sha: string, keys: string[], args: string[]): Promise<unknown>;
323
+ }): (script: string, keys: string[], args: string[]) => Promise<unknown>;
324
+ import { IamDeltaIdMapKey, IamDeltaIdMapStore } from "@ionite/core";
325
+ type RedisIamDeltaIdMapStoreOptions = RedisStoreOptions & {
326
+ /**
327
+ * TTL (seconds) applied to each id-map entry so abandoned externalId mappings (deleted resources,
328
+ * decommissioned destinations) self-evict instead of accumulating forever (§6.7). Each `set`
329
+ * refreshes the TTL, so actively-used mappings persist. Pass `0` to disable expiry.
330
+ */
331
+ idMapTtlSeconds?: number;
332
+ };
333
+ /**
334
+ * Redis-backed {@link IamDeltaIdMapStore}. Durable externalId -> destination provider-id cache for
335
+ * IAM delta `'patch'` egress, shared across app instances and surviving restarts so the patch
336
+ * adapter resolves each resource once and reuses the id thereafter (self-healing on stale ids is
337
+ * handled by the adapter via `delete` + re-resolve).
338
+ */
339
+ declare class RedisIamDeltaIdMapStore implements IamDeltaIdMapStore {
340
+ private readonly redis;
341
+ private readonly prefix;
342
+ private readonly ttlSeconds;
343
+ constructor(options: RedisIamDeltaIdMapStoreOptions);
344
+ private key;
345
+ get(key: IamDeltaIdMapKey): Promise<string | undefined>;
346
+ set(key: IamDeltaIdMapKey, providerId: string): Promise<void>;
347
+ delete(key: IamDeltaIdMapKey): Promise<void>;
348
+ }
349
+ import { Logger as Logger2, SecretsOperationOptions, SecretsSubscriptionOptions } from "@ionite/core";
350
+ /**
351
+ * Options shared by the client-specific `ionRedis(...)` factories in `@ionite/redis` and
352
+ * `@ionite/ioredis`. Each package extends `createReactiveRedis` with only its client glue
353
+ * (client construction, connect/close, endpoint signature); everything below is identical, so it lives
354
+ * here to keep the two adapters in lockstep.
355
+ */
356
+ type IonRedisOptions = {
357
+ sourceName?: string;
358
+ logger?: Logger2;
359
+ secretOptions?: SecretsOperationOptions;
360
+ subscription?: SecretsSubscriptionOptions;
361
+ };
362
+ /**
363
+ * Split a reactive-redis secret into its connection URL and the remaining client options. Both adapters
364
+ * accept the same secret envelope: either a nested `clientOptions` object or the client options spread at
365
+ * the top level, plus an optional `redisUrl`. The `redisUrl`/`clientOptions` envelope keys are stripped
366
+ * from the returned options so they are never forwarded to the underlying client constructor. Callers
367
+ * apply the URL in their client-specific way (node-redis sets `url`; ioredis passes it positionally).
368
+ */
369
+ declare function splitIonRedisSecret(secret: {
370
+ redisUrl?: string;
371
+ clientOptions?: unknown;
372
+ }): {
373
+ redisUrl?: string;
374
+ clientOptions: Record<string, unknown>;
375
+ };
376
+ /** Default page size applied to a paginated `list()` when the caller omits `limit` (§7.3). */
377
+ declare const DEFAULT_LIST_LIMIT = 100;
378
+ /**
379
+ * Hard ceiling on a single `list()` page (§7.3). Must stay >= the `listGenerator` page size (1000) so
380
+ * internal full-enumeration loops that page at that size are never silently truncated.
381
+ */
382
+ declare const MAX_LIST_LIMIT = 1e3;
383
+ import { BaseCustomer as BaseCustomer2, MagicLink, MagicLinkStore } from "@ionite/core";
384
+ declare class RedisMagicLinkStore<TExtended = object> implements MagicLinkStore<TExtended> {
385
+ private readonly redis;
386
+ private readonly prefix;
387
+ private readonly log;
388
+ constructor(options: RedisStoreOptions);
389
+ private key;
390
+ create(token: string, customer: BaseCustomer2, expiresAt: Date): Promise<void>;
391
+ get(token: string): Promise<MagicLink<TExtended> | null>;
392
+ getAndDelete(token: string): Promise<MagicLink<TExtended> | null>;
393
+ delete(token: string): Promise<void>;
394
+ }
395
+ import { Logger as Logger3, Secret, SecretsOperationOptions as SecretsOperationOptions2, SecretsSource, SecretsSubscriptionOptions as SecretsSubscriptionOptions2 } from "@ionite/core";
396
+ type MaybePromise<T> = Promise<T> | T;
397
+ type ReactiveRedisClient = {
398
+ adapter: RedisAdapter;
399
+ dispose?: () => MaybePromise<void>;
400
+ };
401
+ type ReactiveRedisOptions<TSecret> = {
402
+ path: string;
403
+ sourceName?: string;
404
+ source: SecretsSource;
405
+ logger?: Logger3;
406
+ secretOptions?: SecretsOperationOptions2;
407
+ subscription?: SecretsSubscriptionOptions2;
408
+ createClient: (secret: Secret<TSecret>) => MaybePromise<ReactiveRedisClient>;
409
+ /**
410
+ * Optional stable endpoint signature (e.g. `redis://host:port`) derived from a secret (§5.8). When
411
+ * provided, a rotation whose endpoint differs from the live one is rejected (fail-fast) instead of
412
+ * silently repointing mid-flight: credentials may rotate, but the host must not change under an
413
+ * active adapter. Return an empty string when the endpoint is unknown (the guard is then skipped).
414
+ */
415
+ endpointOf?: (secret: Secret<TSecret>) => string;
416
+ };
417
+ type ReactiveRedisAdapter = RedisAdapter & {
418
+ ready(timeout?: number): Promise<void>;
419
+ isReady(): boolean;
420
+ close(): Promise<void>;
421
+ };
422
+ declare function createReactiveRedis<TSecret>(options: ReactiveRedisOptions<TSecret>): ReactiveRedisAdapter;
423
+ import { Session, SessionStore } from "@ionite/core";
424
+ declare class RedisSessionStore<TExtended = object> implements SessionStore<TExtended> {
425
+ private readonly redis;
426
+ private readonly prefix;
427
+ private readonly log;
428
+ constructor(options: RedisStoreOptions);
429
+ private key;
430
+ create(session: Session<TExtended>): Promise<void>;
431
+ get(sid: string): Promise<Session<TExtended> | null>;
432
+ update(sid: string, data: Partial<Session<TExtended>>): Promise<void>;
433
+ delete(sid: string): Promise<void>;
434
+ }
435
+ import { BaseTenant, Ionite, ListOptions as ListOptions5, ListResult as ListResult5, TenantIonCache, TenantStore } from "@ionite/core";
436
+ type RedisTenantStoreOptions = RedisStoreOptions & {
437
+ /**
438
+ * App-owned cache of per-tenant `Ionite` instances (see `TenantIonCache`). Required to
439
+ * use {@link RedisTenantStore.getIon}; the store never constructs runtimes itself. Tenant *records*
440
+ * are plain, non-sensitive Redis lookups; only the runtime (which reads sensitive RemoteConfig from
441
+ * the vault) is cached.
442
+ */
443
+ ionCache?: TenantIonCache;
444
+ };
445
+ declare class RedisTenantStore<TTenant extends BaseTenant = BaseTenant> implements TenantStore<TTenant> {
446
+ private readonly store;
447
+ private readonly ionCache?;
448
+ constructor(options: RedisTenantStoreOptions);
449
+ get(id: string): Promise<TTenant | undefined>;
450
+ upsert(tenant: TTenant): Promise<TTenant>;
451
+ list(options?: ListOptions5): Promise<ListResult5<TTenant>>;
452
+ delete(id: string): Promise<number>;
453
+ getIon(id: string): Promise<Ionite | undefined>;
454
+ }
455
+ import { BaseUser as BaseUser2, ListOptions as ListOptions6, ListResult as ListResult6, UserStore } from "@ionite/core";
456
+ type RedisUserStoreOptions<_TExtended = Record<string, never>> = RedisStoreOptions;
457
+ declare class RedisUserStore<TExtended = Record<string, never>> implements UserStore<TExtended> {
458
+ private readonly redis;
459
+ private readonly prefix;
460
+ private readonly log;
461
+ constructor(options: RedisUserStoreOptions<TExtended>);
462
+ private key;
463
+ private entityKey;
464
+ private idsSetKey;
465
+ private nameIndexKey;
466
+ get(sub: string): Promise<BaseUser2<TExtended> | undefined>;
467
+ lookup(user: BaseUser2<TExtended>): Promise<BaseUser2<TExtended> | undefined>;
468
+ upsert(user: BaseUser2<TExtended>): Promise<BaseUser2<TExtended>>;
469
+ delete(sub: string): Promise<number>;
470
+ list(options?: ListOptions6): Promise<ListResult6<BaseUser2<TExtended>>>;
471
+ }
472
+ import { BaseWorkloadIdentity, ListOptions as ListOptions7, ListResult as ListResult7, WorkloadIdentityStore } from "@ionite/core";
473
+ type RedisWorkloadIdentityStoreOptions = RedisStoreOptions;
474
+ /**
475
+ * Redis-backed {@link WorkloadIdentityStore}. Mirrors the user/customer store layout: a JSON record
476
+ * per identity, an id set for enumeration, a `clientId -> id` index, and optional sorted sets for
477
+ * efficient date-ordered pagination.
478
+ */
479
+ declare class RedisWorkloadIdentityStore<TExtended = Record<string, never>> implements WorkloadIdentityStore<TExtended> {
480
+ private readonly redis;
481
+ private readonly prefix;
482
+ private readonly log;
483
+ constructor(options: RedisWorkloadIdentityStoreOptions);
484
+ private key;
485
+ private entityKey;
486
+ private idsSetKey;
487
+ private clientIndexKey;
488
+ get(id: string): Promise<BaseWorkloadIdentity<TExtended> | undefined>;
489
+ lookup(identity: BaseWorkloadIdentity<TExtended>): Promise<BaseWorkloadIdentity<TExtended> | undefined>;
490
+ upsert(identity: BaseWorkloadIdentity<TExtended>): Promise<BaseWorkloadIdentity<TExtended>>;
491
+ delete(id: string): Promise<number>;
492
+ list(options?: ListOptions7): Promise<ListResult7<BaseWorkloadIdentity<TExtended>>>;
493
+ }
494
+ import { CachedWorkloadToken, WorkloadTokenStore } from "@ionite/core";
495
+ declare class RedisWorkloadTokenStore<TExtended = object> implements WorkloadTokenStore<TExtended> {
496
+ private readonly redis;
497
+ private readonly prefix;
498
+ private readonly log;
499
+ constructor(options: RedisStoreOptions);
500
+ private key;
501
+ set(token: CachedWorkloadToken<TExtended>): Promise<void>;
502
+ get(workload_id: string): Promise<CachedWorkloadToken<TExtended> | null>;
503
+ delete(workload_id: string): Promise<void>;
504
+ isValid(workload_id: string): Promise<boolean>;
505
+ cleanup(): Promise<void>;
506
+ }
507
+ export { supportsRunScript, splitIonRedisSecret, scriptSha1, prefixedKey, getLogger, getKeyPrefix, encodeKeyId, createReactiveRedis, createEvalshaRunner, chunkedMget, atomicCreate, atomicCompareAndSet, RedisWorkloadTokenStore, RedisWorkloadIdentityStoreOptions, RedisWorkloadIdentityStore, RedisUserStoreOptions, RedisUserStore, RedisTxn, RedisTenantStoreOptions, RedisTenantStore, RedisStoreOptions, RedisSessionStore, RedisResourceStore, RedisMagicLinkStore, RedisIamDeltaIdMapStoreOptions, RedisIamDeltaIdMapStore, RedisCustomerStoreOptions, RedisCustomerStore, RedisAdapter, RedisAccessIndex, RedisAccessCatalogStore, RedisAccessAssignmentStore, ReactiveRedisOptions, ReactiveRedisClient, ReactiveRedisAdapter, MAX_LIST_LIMIT, IonRedisOptions, DEFAULT_MGET_CHUNK_SIZE, DEFAULT_LIST_LIMIT, AtomicCreateResult, AtomicCompareAndSetResult };
package/dist/index.js ADDED
@@ -0,0 +1,139 @@
1
+ import{accessRefId as u,accessRootId as kZ,list as $Z}from"@ionite/core";function A(Z){return typeof Z.runScript==="function"}async function T(Z,$,Q,W){let Y=W?.ttlSeconds;if(A(Z)){let q=`local cur=redis.call('GET',KEYS[1])
2
+ if cur then if cur==ARGV[1] then return 'exists_identical' else return 'conflict' end end
3
+ ${Y!=null?"redis.call('SET',KEYS[1],ARGV[1],'EX',ARGV[2])":"redis.call('SET',KEYS[1],ARGV[1])"}
4
+ return 'created'`,B=Y!=null?[Q,String(Math.max(1,Math.floor(Y)))]:[Q],J=String(await Z.runScript(q,[$],B));return J==="exists_identical"||J==="conflict"?J:"created"}let V=await Z.get($);if(V!=null)return V===Q?"exists_identical":"conflict";if(Y!=null)await Z.setex($,Math.max(1,Math.floor(Y)),Q);else await Z.set($,Q);return"created"}async function g(Z,$,Q,W,Y){if(!A(Z))return"unsupported";let V=Y!=null&&Y>0?Math.max(1,Math.floor(Y)):void 0,q=`local cur=redis.call('GET',KEYS[1])
5
+ if not cur then return 'missing' end
6
+ 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'`,B=V!=null?[Q,W,String(V)]:[Q,W],J=String(await Z.runScript(q,[$],B));if(J==="missing"||J==="retry")return J;return"ok"}var y=500,hZ=`local ki=1
9
+ local ai=1
10
+ local C=tonumber(ARGV[ai]);ai=ai+1
11
+ for i=1,C do
12
+ local k=KEYS[ki];ki=ki+1
13
+ local owner=ARGV[ai];ai=ai+1
14
+ local v=redis.call('GET',k)
15
+ if v and v~=owner then return 'conflict' end
16
+ end
17
+ local W=tonumber(ARGV[ai]);ai=ai+1
18
+ for i=1,W do
19
+ local op=ARGV[ai];ai=ai+1
20
+ if op=='set' then
21
+ local k=KEYS[ki];ki=ki+1
22
+ local val=ARGV[ai];ai=ai+1
23
+ redis.call('SET',k,val)
24
+ elseif op=='setex' then
25
+ local k=KEYS[ki];ki=ki+1
26
+ local ttl=ARGV[ai];ai=ai+1
27
+ local val=ARGV[ai];ai=ai+1
28
+ redis.call('SETEX',k,ttl,val)
29
+ elseif op=='del' then
30
+ local k=KEYS[ki];ki=ki+1
31
+ redis.call('DEL',k)
32
+ elseif op=='sadd' then
33
+ local k=KEYS[ki];ki=ki+1
34
+ local m=tonumber(ARGV[ai]);ai=ai+1
35
+ local members={}
36
+ for j=1,m do members[j]=ARGV[ai];ai=ai+1 end
37
+ redis.call('SADD',k,unpack(members))
38
+ elseif op=='srem' then
39
+ local k=KEYS[ki];ki=ki+1
40
+ local m=tonumber(ARGV[ai]);ai=ai+1
41
+ local members={}
42
+ for j=1,m do members[j]=ARGV[ai];ai=ai+1 end
43
+ redis.call('SREM',k,unpack(members))
44
+ end
45
+ end
46
+ return 'ok'`;class L{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(A(Z))return this.execLua(Z);return this.execSequential(Z)}async execLua(Z){if(this.claims.length>0||this.writes.length<=y)return o(Z,this.claims,this.writes);for(let $=0;$<this.writes.length;$+=y)await o(Z,[],this.writes.slice($,$+y));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 o(Z,$,Q){let W=[],Y=[String($.length)];for(let G of $)W.push(G.key),Y.push(G.owner);Y.push(String(Q.length));for(let G of Q)switch(G.op){case"set":Y.push("set"),W.push(G.key),Y.push(G.value);break;case"setex":Y.push("setex"),W.push(G.key),Y.push(String(G.ttl),G.value);break;case"del":Y.push("del"),W.push(G.key);break;case"sadd":Y.push("sadd"),W.push(G.key),Y.push(String(G.members.length),...G.members);break;case"srem":Y.push("srem"),W.push(G.key),Y.push(String(G.members.length),...G.members);break}return String(await Z.runScript(hZ,W,Y))==="conflict"?"conflict":"ok"}import{list as jZ}from"@ionite/core";var r=100,s=1000;function R(Z){let $=Z?.limit,Q=$==null||!Number.isFinite($)||$<=0?r:Math.min(Math.floor($),s);return{...Z,limit:Q}}function _(Z,$,Q=(W,Y)=>W[Y]){let W=Math.max(0,$?.start??0),Y=$?.limit,V=$?.filter?.requestable,G=Z;if(V!==void 0)G=Z.filter((O)=>Boolean(Q(O,"requestable"))===V);let q=G.length,B=Y!=null?W+Y:void 0,J=G.slice(W,B);return jZ(q,J,W,Y)}import{defaultLogger as TZ}from"@ionite/core";function C(Z,$){let Q=$.log??TZ,W={"ion.operation":"redis.revive","ion.outcome":"corrupt",store:$.store,...$.key?{key:$.key}:{}},Y;try{Y=JSON.parse(Z)}catch{Q.warn("Redis store: discarded corrupt JSON payload (failed to parse)",W);return}if(Y===null||typeof Y!=="object"||Array.isArray(Y)){Q.warn("Redis store: discarded JSON payload that is not a record object",W);return}return Y}import{createHash as vZ}from"node:crypto";import{defaultLogger as wZ}from"@ionite/core";var IZ=4096,t=new Set,e=new Set;function ZZ(Z,$){Z.delete($),Z.add($);while(Z.size>IZ){let Q=Z.values().next().value;if(Q===void 0)break;Z.delete(Q)}}function F(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. "es:t:<tenantId>:") so tenant keyspaces never collide.');if($){let W=`${$}\x00${Q}`;if(t.has(W)&&!e.has(W))D(Z).warn(`Redis ${$} constructed twice with the same keyPrefix "${Q}". Each tenant MUST use a distinct keyPrefix (e.g. "es:t:<tenantId>:") so tenant keyspaces never collide; sharing a prefix across tenants lets them read/write each other's data.`),ZZ(e,W);ZZ(t,W)}return Q}function D(Z){return Z.logger??wZ}function X(Z,...$){return Z+$.join(":")}var mZ=/^[A-Za-z0-9_.@|-]{1,64}$/;function H(Z){if(mZ.test(Z))return`=${Z}`;return`#${vZ("sha256").update(Z,"utf8").digest("base64url").slice(0,22)}`}var bZ="access:ids",yZ="access:subject:",gZ="access:target:";function lZ(Z){return JSON.stringify({...Z,createdAt:Z.createdAt.toISOString(),updatedAt:Z.updatedAt.toISOString()})}function l(Z,$){let Q=C(Z,{store:"access",log:$});if(Q===void 0)return;return{...Q,createdAt:new Date(Q.createdAt),updatedAt:new Date(Q.updatedAt)}}function QZ(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 WZ(Z){let $=u(Z.subject),Q=u(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??kZ(Z),subjectId:$,targetId:Q}}class YZ{redis;prefix;log;constructor(Z){this.redis=Z.redis,this.prefix=F(Z,"access-assignment-store"),this.log=D(Z)}key(...Z){return X(this.prefix,...Z)}idsSetKey(){return this.key(bZ)}accessStorageKey(Z){return this.key("access",H(Z))}subjectIndexKey(Z,$){return this.key(`${yZ}${$??"user"}:${H(Z)}`)}targetIndexKey(Z,$){return this.key(gZ+Z,H($))}async grant(Z){let $=WZ(Z),Q=this.accessStorageKey($.id),W=await this.redis.get(Q),Y=W?l(W,this.log):void 0,V=new Date,G={...Z,id:$.id,status:Z.status??"active",subjectId:$.subjectId,targetId:$.targetId,createdAt:Y?.createdAt??V,updatedAt:V},q=new L;q.set(Q,lZ(G)),q.sadd(this.idsSetKey(),$.id),q.sadd(this.subjectIndexKey($.subjectId,Z.subjectType),$.id),q.sadd(this.targetIndexKey(Z.targetType,$.targetId),$.id),await q.exec(this.redis)}async revoke(Z){let $=WZ(Z);if(await this.redis.get(this.accessStorageKey($.id))==null)return;let W=new L;W.del(this.accessStorageKey($.id)),W.srem(this.idsSetKey(),$.id),W.srem(this.subjectIndexKey($.subjectId,Z.subjectType),$.id),W.srem(this.targetIndexKey(Z.targetType,$.targetId),$.id),await W.exec(this.redis)}async get(Z){let $=await this.redis.get(this.accessStorageKey(Z));if($==null)return;let Q=l($,this.log);return Q?QZ(Q):void 0}async list(Z){return this.listInMemory(R(Z))}async listByTarget(Z,$,Q){let W=await this.redis.smembers(this.targetIndexKey(Z,$.trim()));return this.listFromIds(W,Q)}async listBySubject(Z,$,Q){let W=u(Z);if(!W)return $Z(0,[],Q?.start??0,Q?.limit);let Y=await this.redis.smembers(this.subjectIndexKey(W,$));return this.listFromIds(Y,Q)}async listInMemory(Z){let $=await this.redis.smembers(this.idsSetKey());return this.listFromIds($,Z)}async listFromIds(Z,$){if(Z.length===0)return $Z(0,[],$?.start??0,$?.limit);let W=[...Z].sort().map((q)=>this.accessStorageKey(q)),V=(await this.redis.mget(W)).filter((q)=>q!=null).map((q)=>l(q,this.log)).filter((q)=>q!=null).filter((q)=>(q.status??"active")==="active"),G=_(V,$);return{...G,items:G.items.map(QZ)}}}import{readyCatalogEntry as uZ}from"@ionite/core";class GZ{groups;roles;permissions;listeners=new Set;constructor(Z){let $=Z.redis,Q=F(Z,"access-catalog-store"),W=D(Z),Y=(V)=>{for(let G of this.listeners)G(V)};this.groups=new I($,Q,"group",Y,W),this.roles=new I($,Q,"role",Y,W),this.permissions=new I($,Q,"permission",Y,W)}afterChange(Z){return this.listeners.add(Z),()=>{this.listeners.delete(Z)}}}class VZ{redis;prefix;log;listeners=new Set;entitlements;entitlement;constructor(Z){if(this.redis=Z.redis,this.prefix=F(Z,"resource-store"),this.log=D(Z),Z.deriveEntitlements){let{deriveEntitlements:$,resolveEntitlement:Q}=Z;if(!Q)throw Error("RedisResourceStore: resolveEntitlement must be provided with deriveEntitlements.");this.entitlements=(W)=>$(W),this.entitlement=(W)=>Q(W,(Y)=>this.get(Y))}}key(...Z){return X(this.prefix,"authz","resource",...Z)}entityKey(Z){return this.key(H(Z))}idsSetKey(){return this.key("ids")}async get(Z){let $=await this.redis.get(this.entityKey(Z));if($==null)return;let Q=C($,{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 _([],Z);let W=(await this.redis.mget($.map((Y)=>this.entityKey(Y)))).filter((Y)=>Y!=null).map((Y)=>C(Y,{store:"authz:resource",log:this.log})).filter((Y)=>Y!==void 0).map((Y)=>Y);return _(W,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 L().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 L().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,W,Y){this.redis=Z;this.prefix=$;this.kind=Q;this.emit=W;this.log=Y}key(...Z){return X(this.prefix,"authz",this.kind,...Z)}idsSetKey(){return this.key("ids")}entityKey(Z){return this.key(H(Z))}externalIdKey(Z){return this.key("byExternalId",H(Z))}async get(Z){let $=await this.redis.get(this.entityKey(Z));if($==null)return;let Q=C($,{store:`authz:${this.kind}`,key:Z,log:this.log});return Q===void 0?void 0:Q}async getByExternalId(Z){let $=await this.redis.get(this.externalIdKey(Z));if($==null)return;return this.get($)}set(Z){let $=(async()=>{let Q=await this.get(Z.id),W=new L;if(W.set(this.entityKey(Z.id),JSON.stringify(Z)),W.sadd(this.idsSetKey(),Z.id),Q?.externalId&&Q.externalId!==Z.externalId)W.del(this.externalIdKey(Q.externalId));if(Z.externalId)W.set(this.externalIdKey(Z.externalId),Z.id);await W.exec(this.redis)})();return $.then(()=>this.emit({op:"set",kind:this.kind,entity:Z}),(Q)=>{this.log?.warn("authz catalog store: background persist failed",{"ion.operation":`authz.catalog.${this.kind}.set`,"ion.outcome":"error",id:Z.id,error:Q instanceof Error?Q.message:String(Q)})}),uZ(Z,$)}async delete(Z){let $=await this.get(Z),Q=new L;if(Q.del(this.entityKey(Z)),Q.srem(this.idsSetKey(),Z),$?.externalId)Q.del(this.externalIdKey($.externalId));await Q.exec(this.redis),this.emit({op:"delete",kind:this.kind,id:Z})}async list(Z){let $=await this.redis.smembers(this.idsSetKey());if($.length===0)return _([],Z);let W=(await this.redis.mget($.map((Y)=>this.entityKey(Y)))).filter((Y)=>Y!=null).map((Y)=>C(Y,{store:`authz:${this.kind}`,log:this.log})).filter((Y)=>Y!==void 0).map((Y)=>Y);return _(W,Z)}}import{accessRefId as P}from"@ionite/core";var m="__app__",qZ=100;function pZ(Z){if(!Array.isArray(Z))return[];return Z.map(($)=>$==null?"":String($)).filter(($)=>$.length>0)}var cZ=`local rootId=ARGV[1]
47
+ local A=tonumber(ARGV[2])
48
+ local R=tonumber(ARGV[3])
49
+ local rootKey=KEYS[1]
50
+ local subjectsKey=KEYS[2]
51
+ for i=0,A-1 do
52
+ local kb=2+i*5
53
+ local er=KEYS[kb+1]
54
+ local hk=KEYS[kb+2]
55
+ local tk=KEYS[kb+3]
56
+ local ak=KEYS[kb+4]
57
+ local sk=KEYS[kb+5]
58
+ local ab=3+i*3
59
+ local descriptor=ARGV[ab+1]
60
+ local rToken=ARGV[ab+2]
61
+ local subjectMember=ARGV[ab+3]
62
+ redis.call('SADD',er,rootId)
63
+ redis.call('SET',hk,'1')
64
+ redis.call('SADD',tk,rToken)
65
+ redis.call('SET',ak,'1')
66
+ redis.call('SADD',rootKey,descriptor)
67
+ redis.call('SADD',sk,descriptor)
68
+ redis.call('SADD',subjectsKey,subjectMember)
69
+ end
70
+ local rkb=2+A*5
71
+ local rab=3+A*3
72
+ for i=0,R-1 do
73
+ local kb=rkb+i*5
74
+ local er=KEYS[kb+1]
75
+ local hk=KEYS[kb+2]
76
+ local tk=KEYS[kb+3]
77
+ local ak=KEYS[kb+4]
78
+ local sk=KEYS[kb+5]
79
+ local ab=rab+i*2
80
+ local descriptor=ARGV[ab+1]
81
+ local rToken=ARGV[ab+2]
82
+ redis.call('SREM',er,rootId)
83
+ if redis.call('SCARD',er)==0 then
84
+ redis.call('DEL',er)
85
+ redis.call('DEL',hk)
86
+ redis.call('SREM',sk,descriptor)
87
+ redis.call('SREM',tk,rToken)
88
+ if redis.call('SCARD',tk)==0 then
89
+ redis.call('DEL',tk)
90
+ redis.call('DEL',ak)
91
+ end
92
+ end
93
+ redis.call('SREM',rootKey,descriptor)
94
+ end
95
+ return 'ok'`,dZ=`local rootId=ARGV[1]
96
+ local n=tonumber(ARGV[2])
97
+ for i=0,n-1 do
98
+ local kb=1+i*5
99
+ local er=KEYS[kb+1]
100
+ local hk=KEYS[kb+2]
101
+ local tk=KEYS[kb+3]
102
+ local ak=KEYS[kb+4]
103
+ local sk=KEYS[kb+5]
104
+ local descriptor=ARGV[3+i*2]
105
+ local rToken=ARGV[4+i*2]
106
+ redis.call('SREM',er,rootId)
107
+ if redis.call('SCARD',er)==0 then
108
+ redis.call('DEL',er)
109
+ redis.call('DEL',hk)
110
+ redis.call('SREM',sk,descriptor)
111
+ redis.call('SREM',tk,rToken)
112
+ if redis.call('SCARD',tk)==0 then
113
+ redis.call('DEL',tk)
114
+ redis.call('DEL',ak)
115
+ end
116
+ end
117
+ end
118
+ redis.call('DEL',KEYS[1])
119
+ return 'ok'`,nZ=`local n=tonumber(ARGV[1])
120
+ local roots={}
121
+ local seen={}
122
+ for i=0,n-1 do
123
+ local kb=1+i*4
124
+ local er=KEYS[kb+1]
125
+ local hk=KEYS[kb+2]
126
+ local tk=KEYS[kb+3]
127
+ local ak=KEYS[kb+4]
128
+ local members=redis.call('SMEMBERS',er)
129
+ for _,r in ipairs(members) do
130
+ if not seen[r] then seen[r]=true roots[#roots+1]=r end
131
+ end
132
+ redis.call('DEL',er)
133
+ redis.call('DEL',hk)
134
+ redis.call('DEL',tk)
135
+ redis.call('DEL',ak)
136
+ end
137
+ redis.call('DEL',KEYS[1])
138
+ return roots`,iZ=`for i=1,#KEYS do redis.call('DEL',KEYS[i]) end
139
+ return 'ok'`;function S(Z,$){return`${$}:${Z}`}function p(Z){return Z&&Z.length>0?Z:m}function BZ(Z){return JSON.stringify([Z.subject,Z.targetType,Z.target,Z.rToken])}function v(Z){let $;try{$=JSON.parse(Z)}catch{$=[]}let[Q,W,Y,V]=Array.isArray($)?$:[];return{subject:Q??"",targetType:W??"permission",target:Y??"",rToken:V??m}}class UZ{redis;prefix;log;constructor(Z){this.redis=Z.redis,this.prefix=F(Z,"access-index"),this.log=D(Z)}key(...Z){return X(this.prefix,...Z)}subjectKeyPart(Z){let $=Z.indexOf(":");if($<=0)return H(Z);return`${Z.slice(0,$)}:${H(Z.slice($+1))}`}hasKey(Z,$,Q,W){return this.key("idx:has",this.subjectKeyPart(Z),$,H(Q),H(W))}anyKey(Z,$,Q){return this.key("idx:any",this.subjectKeyPart(Z),$,H(Q))}edgeRootsKey(Z,$,Q,W){return this.key("idx:er",this.subjectKeyPart(Z),$,H(Q),H(W))}tripleKey(Z,$,Q){return this.key("idx:tr",this.subjectKeyPart(Z),$,H(Q))}rootKey(Z){return this.key("idx:root",H(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,W,Y="user"){let V=P(Z),G=P(Q);if(!V||!G)return{allow:!1};let q=S(V,Y);if(W===void 0){let[O,N]=await this.redis.mget([this.disabledKey(q),this.anyKey(q,$,G)]);if(O!=null)return{allow:!1,metadata:{reason:"subject_disabled"}};return N!=null?this.allow(V,$,G,void 0):{allow:!1}}let B=P(W),J=await this.redis.mget([this.disabledKey(q),this.hasKey(q,$,G,p(B)),this.hasKey(q,$,G,m)]);if(J[0]!=null)return{allow:!1,metadata:{reason:"subject_disabled"}};if(J[1]!=null)return this.allow(V,$,G,B);if(J[2]!=null)return this.allow(V,$,G,void 0);return{allow:!1}}async hasGroup(Z,$,Q,W="user"){return this.has(Z,"group",$,Q,W)}async hasRole(Z,$,Q,W="user"){return this.has(Z,"role",$,Q,W)}async hasPermission(Z,$,Q,W="user"){return this.has(Z,"permission",$,Q,W)}async hasAny(Z,$,Q="user"){if($.length===0)return{allow:!1};this.warnLargeCheck("redis.access_index.has_any",$.length);let W=await this.evaluateTargets(Z,$,Q);for(let Y of W)if(Y.allow)return Y;return{allow:!1}}async hasAll(Z,$,Q="user"){if($.length===0){let V=P(Z);if(V){if(await this.redis.get(this.disabledKey(S(V,Q)))!=null)return{allow:!1,metadata:{reason:"subject_disabled"}}}return{allow:!0}}this.warnLargeCheck("redis.access_index.has_all",$.length);let W=await this.evaluateTargets(Z,$,Q),Y=[];for(let V of W){if(!V.allow)return{allow:!1,matched:Y};if(V.matched)Y.push(...V.matched)}return{allow:!0,matched:Y}}async evaluateTargets(Z,$,Q){let W=P(Z);if(!W)return $.map(()=>({allow:!1}));let Y=S(W,Q),V=[this.disabledKey(Y)],G=$.map((B)=>{let J=P(B.target);if(!J)return{kind:"invalid"};if(B.resource===void 0){let f=V.push(this.anyKey(Y,B.targetType,J))-1;return{kind:"any",targetType:B.targetType,targetId:J,idx:f}}let O=P(B.resource),N=V.push(this.hasKey(Y,B.targetType,J,p(O)))-1,E=V.push(this.hasKey(Y,B.targetType,J,m))-1;return{kind:"resource",targetType:B.targetType,targetId:J,resourceId:O,idxResource:N,idxApp:E}}),q=await this.redis.mget(V);if(q[0]!=null)return $.map(()=>({allow:!1,metadata:{reason:"subject_disabled"}}));return G.map((B)=>{if(B.kind==="invalid")return{allow:!1};if(B.kind==="any")return q[B.idx]!=null?this.allow(W,B.targetType,B.targetId,void 0):{allow:!1};if(q[B.idxResource]!=null)return this.allow(W,B.targetType,B.targetId,B.resourceId);if(q[B.idxApp]!=null)return this.allow(W,B.targetType,B.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 q=P(G.subject),B=P(G.target);if(!q||!B)continue;let J=S(q,G.subjectType??"user"),O=p(G.resource),N={subject:J,targetType:G.targetType,target:B,rToken:O};Q.set(BZ(N),N)}let W=new Set(await this.redis.smembers(this.rootKey(Z))),Y=[];for(let[G,q]of Q)if(!W.has(G))Y.push({encoded:G,d:q});let V=[];for(let G of W)if(!Q.has(G))V.push({encoded:G,d:v(G)});if(Y.length===0&&V.length===0)return;if(A(this.redis)){let G=[this.rootKey(Z),this.subjectsKey()],q=[Z,String(Y.length),String(V.length)];for(let{encoded:B,d:J}of Y)G.push(...this.edgeKeys(J)),q.push(B,J.rToken,J.subject);for(let{encoded:B,d:J}of V)G.push(...this.edgeKeys(J)),q.push(B,J.rToken);await this.redis.runScript(cZ,G,q);return}for(let{encoded:G,d:q}of Y)await this.redis.sadd(this.edgeRootsKey(q.subject,q.targetType,q.target,q.rToken),Z),await this.redis.set(this.hasKey(q.subject,q.targetType,q.target,q.rToken),"1"),await this.redis.sadd(this.tripleKey(q.subject,q.targetType,q.target),q.rToken),await this.redis.set(this.anyKey(q.subject,q.targetType,q.target),"1"),await this.redis.sadd(this.rootKey(Z),G),await this.redis.sadd(this.subjectKey(q.subject),G),await this.redis.sadd(this.subjectsKey(),q.subject);for(let{encoded:G,d:q}of V)await this.dropRootFromEdge(Z,q.subject,q.targetType,q.target,q.rToken),await this.redis.srem(this.rootKey(Z),G)}async removeByRoot(Z){let $=this.rootKey(Z),Q=await this.redis.smembers($);if(Q.length===0){await this.redis.del($);return}if(A(this.redis)){let W=[$],Y=[Z,String(Q.length)];for(let V of Q){let G=v(V);W.push(...this.edgeKeys(G)),Y.push(V,G.rToken)}await this.redis.runScript(dZ,W,Y);return}for(let W of Q){let{subject:Y,targetType:V,target:G,rToken:q}=v(W);await this.dropRootFromEdge(Z,Y,V,G,q)}await this.redis.del($)}async listSubjects(){let Z=await this.redis.smembers(this.subjectsKey()),$=[];for(let Q of Z){let W=Q.indexOf(":");if(W<=0){$.push({subject:Q});continue}$.push({subjectType:Q.slice(0,W),subject:Q.slice(W+1)})}return $}async setSubjectDisabled(Z,$,Q){let W=P(Z);if(!W)return;let Y=this.disabledKey(S(W,$));if(Q)await this.redis.set(Y,"1");else await this.redis.del(Y)}async isSubjectDisabled(Z,$="user"){let Q=P(Z);if(!Q)return!1;return await this.redis.get(this.disabledKey(S(Q,$)))!=null}async clear(Z,$="user"){if(Z===void 0){let Y=await this.redis.smembers(this.subjectsKey());for(let V of Y)await this.clearSubject(V);await this.redis.del(this.subjectsKey());return}let Q=P(Z);if(!Q)return;let W=S(Q,$);await this.clearSubject(W),await this.redis.srem(this.subjectsKey(),W)}async clearSubject(Z){let $=this.subjectKey(Z),Q=await this.redis.smembers($);if(Q.length===0){await this.redis.del($);return}if(A(this.redis)){let W=[$];for(let V of Q){let{targetType:G,target:q,rToken:B}=v(V);W.push(this.edgeRootsKey(Z,G,q,B),this.hasKey(Z,G,q,B),this.tripleKey(Z,G,q),this.anyKey(Z,G,q))}let Y=pZ(await this.redis.runScript(nZ,W,[String(Q.length)]));if(Y.length>0)await this.redis.runScript(iZ,Y.map((V)=>this.rootKey(V)),[]);return}for(let W of Q){let{targetType:Y,target:V,rToken:G}=v(W),q=this.edgeRootsKey(Z,Y,V,G),B=await this.redis.smembers(q);for(let O of B)await this.redis.srem(this.rootKey(O),W);if(await this.redis.del(q),await this.redis.del(this.hasKey(Z,Y,V,G)),await this.redis.srem(this.tripleKey(Z,Y,V),G),(await this.redis.smembers(this.tripleKey(Z,Y,V))).length===0)await this.redis.del(this.tripleKey(Z,Y,V)),await this.redis.del(this.anyKey(Z,Y,V))}await this.redis.del($)}async dropRootFromEdge(Z,$,Q,W,Y){let V=this.edgeRootsKey($,Q,W,Y);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,W,Y));let q=BZ({subject:$,targetType:Q,target:W,rToken:Y});if(await this.redis.srem(this.subjectKey($),q),await this.redis.srem(this.tripleKey($,Q,W),Y),(await this.redis.smembers(this.tripleKey($,Q,W))).length===0)await this.redis.del(this.tripleKey($,Q,W)),await this.redis.del(this.anyKey($,Q,W))}allow(Z,$,Q,W){return{allow:!0,matched:[{subject:Z,targetType:$,target:Q,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 aZ=512;async function oZ(Z,$,Q=512){if(Z.length===0)return[];let W=Q>0?Q:512,Y=[];for(let V=0;V<Z.length;V+=W){let G=await $(Z.slice(V,V+W));for(let q of G)Y.push(q??null)}return Y}import{list as rZ,StoreConflictError as sZ}from"@ionite/core";var tZ="customer:ids",eZ="customer:name:";function Z$(Z){let $=Z.createdAt??new Date,Q=Z.updatedAt??$,W={...Z,createdAt:$.toISOString(),updatedAt:Q.toISOString()};if(W.ciam&&typeof W.ciam==="object"&&W.ciam!==null){let Y=W.ciam;if(Y.expires instanceof Date)Y.expires=Y.expires.toISOString()}return JSON.stringify(W)}function c(Z,$){let Q=C(Z,{store:"customer",log:$});if(Q===void 0)return;if(Q.ciam&&typeof Q.ciam==="object"&&Q.ciam!==null){let W=Q.ciam;if(typeof W.expires==="string")W.expires=new Date(W.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=F(Z,"customer-store"),this.log=D(Z)}key(...Z){return X(this.prefix,...Z)}entityKey(Z){return this.key("customer",H(Z))}idsSetKey(){return this.key(tZ)}nameIndexKey(Z){return this.key(eZ+H(Z))}async get(Z){let $=await this.redis.get(this.entityKey(Z));if($==null)return;return c($,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($)}return}async upsert(Z){let $=Z.id?.trim()||crypto.randomUUID(),Q={...Z,id:$},W=this.entityKey($),Y=await this.redis.get(W),V=Y!=null?c(Y,this.log)??null:null,G=Q.userName?.toLowerCase(),q=V?.userName?.toLowerCase(),B=new L;if(G)B.claimUnique(this.nameIndexKey(G),$);if(q&&q!==G)B.del(this.nameIndexKey(q));if(B.set(W,Z$(Q)),G)B.set(this.nameIndexKey(G),$);if(B.sadd(this.idsSetKey(),$),await B.exec(this.redis)==="conflict")throw new sZ(`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 L;if(Q.del(this.entityKey(Z)),$.userName)Q.del(this.nameIndexKey($.userName.toLowerCase()));return Q.srem(this.idsSetKey(),Z),await Q.exec(this.redis),1}async list(Z){let $=R(Z),Q=(await this.redis.smembers(this.idsSetKey())).sort();if(Q.length===0)return rZ(0,[],$.start??0,$.limit);let W=Q.map((G)=>this.entityKey(G)),V=(await this.redis.mget(W)).filter((G)=>G!=null).map((G)=>c(G,this.log)).filter((G)=>G!=null);return _(V,$)}}import{createHash as $$}from"node:crypto";function JZ(Z){return $$("sha1").update(Z,"utf8").digest("hex")}function Q$(Z){return(Z instanceof Error?Z.message:String(Z)).includes("NOSCRIPT")}function W$(Z){let $=new Map;return async(Q,W,Y)=>{let V=$.get(Q);if(!V)V=JZ(Q),$.set(Q,V);try{return await Z.evalsha(V,W,Y)}catch(G){if(Q$(G))return Z.eval(Q,W,Y);throw G}}}var Y$=7776000;class MZ{redis;prefix;ttlSeconds;constructor(Z){this.redis=Z.redis,this.prefix=F(Z,"iam-delta-id-map-store");let $=Z.idMapTtlSeconds;this.ttlSeconds=$==null?Y$:Math.max(0,Math.floor($))}key(Z){return X(this.prefix,"iam","delta","idmap",H(Z.tenantId??"_"),H(Z.resourceType),H(Z.destination),H(Z.externalId))}async get(Z){return await this.redis.get(this.key(Z))??void 0}async set(Z,$){if(this.ttlSeconds>0)await this.redis.setex(this.key(Z),this.ttlSeconds,$);else await this.redis.set(this.key(Z),$)}async delete(Z){await this.redis.del(this.key(Z))}}function FZ(Z){return typeof Z==="object"&&Z!==null&&!Array.isArray(Z)}function G$(Z){let $=FZ(Z.clientOptions)?{...Z.clientOptions}:FZ(Z)?{...Z}:{};return delete $.redisUrl,delete $.clientOptions,{redisUrl:(typeof Z.redisUrl==="string"?Z.redisUrl.trim():"")||void 0,clientOptions:$}}import{StoreConflictError as V$}from"@ionite/core";function q$(Z){return JSON.stringify({token:Z.token,customer:Z.customer,createdAt:Z.createdAt.toISOString(),expiresAt:Z.expiresAt.toISOString()})}function XZ(Z,$){let Q=C(Z,{store:"magic-link",log:$});if(Q===void 0)return;return{...Q,createdAt:new Date(Q.createdAt),expiresAt:new Date(Q.expiresAt)}}class OZ{redis;prefix;log;constructor(Z){this.redis=Z.redis,this.prefix=F(Z,"magic-link-store"),this.log=D(Z)}key(Z){return X(this.prefix,"magic",H(Z))}async create(Z,$,Q){let W=this.key(Z),Y={token:Z,customer:$,createdAt:new Date,expiresAt:Q},V=Math.max(1,Math.floor((Q.getTime()-Date.now())/1000));if(await T(this.redis,W,q$(Y),{ttlSeconds:V})==="conflict")throw new V$(`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 XZ($,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 W=XZ(Q,this.log);if(W==null)return null;if(Date.now()>W.expiresAt.getTime())return null;return W}async delete(Z){await this.redis.del(this.key(Z))}}import{defaultLogger as B$}from"@ionite/core";function U$(Z,$){if(typeof $!=="number"||$<=0)return Z;return new Promise((Q,W)=>{let Y=setTimeout(()=>{W(Error(`Reactive Redis adapter did not become ready within ${$}ms`))},$);Z.then(()=>{clearTimeout(Y),Q()},(V)=>{clearTimeout(Y),W(V)})})}function H$(){let Z,$;return{promise:new Promise((W,Y)=>{Z=W,$=Y}),resolve:Z,reject:$}}function zZ(Z){return typeof Z==="function"}function J$(Z){let $=Z.logger??B$,Q=Z.sourceName?.trim()||"redis",W=Z.path.trim(),Y,V,G=!1,q,B,J,O=!1,N=!1,E=!1,f=Promise.resolve(),x=H$();x.promise.catch(()=>{});let j={async get(U){return K((M)=>M.get(U))},async set(U,M){await K((z)=>z.set(U,M))},async setex(U,M,z){await K((h)=>h.setex(U,M,z))},async del(U){await K((M)=>M.del(U))},async sadd(U,...M){await K((z)=>z.sadd(U,...M))},async srem(U,...M){await K((z)=>z.srem(U,...M))},async smembers(U){return K((M)=>M.smembers(U))},async mget(U){return K((M)=>M.mget(U))},ready(U){return U$(x.promise,U)},isReady(){return Y!==void 0},async close(){if(O=!0,a(),!G)G=!0,x.reject(Error(`Reactive Redis adapter for source "${Q}" at path "${W}" was closed before becoming ready.`));await f.catch(()=>{}),await k(Y),Y=void 0,b()}};function i(U){f=f.then(U).catch((M)=>{if(!G)G=!0,x.reject(M);$.warn("Reactive Redis adapter update failed",{"ion.operation":"redis.adapter.update","ion.outcome":"degraded",sourceName:Q,path:W,error:M instanceof Error?M.message:String(M)})})}async function SZ(){if(O)throw Error(`Reactive Redis adapter for source "${Q}" at path "${W}" is closed.`);let U=Y?.adapter;if(U)return U;if(await f.catch(()=>{}),O)throw Error(`Reactive Redis adapter for source "${Q}" at path "${W}" is closed.`);let M=Y?.adapter;if(M)return M;if(await x.promise,O)throw Error(`Reactive Redis adapter for source "${Q}" at path "${W}" is closed.`);let z=Y?.adapter;if(!z)throw Error(`Reactive Redis adapter for source "${Q}" at path "${W}" did not become ready.`);return z}async function K(U){let M=await SZ();return U(M)}function a(){V?.(),V=void 0}async function fZ(U){a(),V=await U.subscribe(W,(M)=>{i(async()=>{if(O)return;await xZ(M)})},Z.subscription??Z.secretOptions)}async function xZ(U){if(O)return;let M=`${Q}:${W}:${U.metadata.version}`;if(Y!==void 0&&B===M&&q===U.metadata.version)return;let z=Z.endpointOf?.(U);if(Y!==void 0&&z!=null&&z!==""&&J!=null&&J!==""&&z!==J)throw Error(`Reactive Redis adapter for source "${Q}" at path "${W}" rejected a rotation that changed the endpoint (${J} -> ${z}); only credentials may rotate under an active adapter.`);let h=await Z.createClient(U);if(O){await k(h);return}let w=Y;if(Y=h,B=M,q=U.metadata.version,z!=null&&z!=="")J=z;if(b(),!G)G=!0,x.resolve();await k(w)}async function k(U){if(!U?.dispose)return;await U.dispose()}function b(){let U=Y?.adapter;if(U?.runScript&&zZ(U.runScript))j.runScript=async(M,z,h)=>{return K(async(w)=>{if(!w.runScript)throw Error("Active Redis adapter does not support runScript");return w.runScript(M,z,h)})};else if(delete j.runScript,U!==void 0&&!N)N=!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:W,capability:"runScript"});if(U?.getdel&&zZ(U.getdel))j.getdel=async(M)=>{return K(async(z)=>{if(!z.getdel)return null;return z.getdel(M)})};else if(delete j.getdel,U!==void 0&&!E)E=!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:W,capability:"getdel"})}return b(),i(async()=>{if(O)return;await fZ(Z.source)}),j}import{StoreConflictError as M$}from"@ionite/core";var F$=5;function X$(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 DZ(Z){return JSON.stringify({...Z,createdAt:Z.createdAt.toISOString()})}function LZ(Z,$){let Q=C(Z,{store:"session",log:$});if(Q===void 0)return;return{...Q,createdAt:new Date(Q.createdAt)}}function CZ(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 _Z{redis;prefix;log;constructor(Z){this.redis=Z.redis,this.prefix=F(Z,"session-store"),this.log=D(Z)}key(Z){return X(this.prefix,"session",H(Z))}async create(Z){let $=this.key(Z.sid),Q=CZ(Z.expiresAt);if(await T(this.redis,$,DZ(Z),{ttlSeconds:Q})==="conflict")throw new M$(`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 LZ($,this.log)??null}async update(Z,$){let Q=this.key(Z),W=JSON.parse(JSON.stringify($)),Y="expiresAt"in W?CZ(W.expiresAt):void 0;if(A(this.redis))for(let J=0;J<F$;J++){let O=await this.redis.get(Q);if(O==null)throw Error(`Session with sid ${Z} not found`);let N=X$(O,W);if(N===void 0)throw Error(`Session with sid ${Z} not found`);let E=await g(this.redis,Q,O,N,Y);if(E==="ok")return;if(E==="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=LZ(V,this.log);if(G==null)throw Error(`Session with sid ${Z} not found`);let q={...G,...$},B=DZ(q);if(Y!=null&&Y>0)await this.redis.setex(Q,Math.max(1,Math.floor(Y)),B);else await this.redis.set(Q,B)}async delete(Z){await this.redis.del(this.key(Z))}}var O$="tenant:ids";function z$(Z){return JSON.stringify({...Z,createdAt:Z.createdAt.toISOString(),updatedAt:Z.updatedAt.toISOString()})}function NZ(Z,$){let Q=C(Z,{store:"tenant",log:$});if(Q===void 0)return;return{...Q,createdAt:new Date(Q.createdAt),updatedAt:new Date(Q.updatedAt)}}class PZ{redis;prefix;log;constructor(Z){this.redis=Z.redis,this.prefix=F(Z,"tenant-store"),this.log=D(Z)}key(...Z){return X(this.prefix,...Z)}entityKey(Z){return this.key("tenant",H(Z))}idsSetKey(){return this.key(O$)}async get(Z){let $=await this.redis.get(this.entityKey(Z));if($==null)return;return NZ($,this.log)}async upsert(Z){return await new L().set(this.entityKey(Z.id),z$(Z)).sadd(this.idsSetKey(),Z.id).exec(this.redis),Z}async list(Z){let $=await this.redis.smembers(this.idsSetKey());if($.length===0)return _([],Z);let Q=$.map((V)=>this.entityKey(V)),Y=(await this.redis.mget(Q)).map((V)=>V!=null?NZ(V,this.log):void 0).filter((V)=>V!=null);return _(Y,Z)}async delete(Z){let $=await this.get(Z);return await new L().srem(this.idsSetKey(),Z).del(this.entityKey(Z)).exec(this.redis),$?1:0}}class AZ{store;ionCache;constructor(Z){this.ionCache=Z.ionCache,this.store=new PZ(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 D$,StoreConflictError as L$}from"@ionite/core";var C$="user:ids",_$="user:name:";function N$(Z){let $=Z.createdAt??new Date,Q=Z.updatedAt??$,W={...Z,createdAt:$.toISOString(),updatedAt:Q.toISOString()};if(W.sso&&typeof W.sso==="object"&&W.sso!==null){let Y=W.sso;if(Y.expires instanceof Date)Y.expires=Y.expires.toISOString()}return JSON.stringify(W)}function d(Z,$){let Q=C(Z,{store:"user",log:$});if(Q===void 0)return;if(Q.sso&&typeof Q.sso==="object"&&Q.sso!==null){let W=Q.sso;if(typeof W.expires==="string")W.expires=new Date(W.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 KZ{redis;prefix;log;constructor(Z){this.redis=Z.redis,this.prefix=F(Z,"user-store"),this.log=D(Z)}key(...Z){return X(this.prefix,...Z)}entityKey(Z){return this.key("user",H(Z))}idsSetKey(){return this.key(C$)}nameIndexKey(Z){return this.key(_$+H(Z))}async get(Z){let $=await this.redis.get(this.entityKey(Z));if($==null)return;return d($,this.log)}async lookup(Z){return this.get(Z.id??"")}async upsert(Z){let $=Z.id?.trim()||crypto.randomUUID(),Q={...Z,id:$},W=this.entityKey($),Y=await this.redis.get(W),V=Y!=null?d(Y,this.log)??null:null,G=Q.userName?.toLowerCase(),q=V?.userName?.toLowerCase(),B=new L;if(G)B.claimUnique(this.nameIndexKey(G),$);if(q&&q!==G)B.del(this.nameIndexKey(q));if(B.set(W,N$(Q)),G)B.set(this.nameIndexKey(G),$);if(B.sadd(this.idsSetKey(),$),await B.exec(this.redis)==="conflict")throw new L$(`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 L;if(Q.del(this.entityKey(Z)),$.userName)Q.del(this.nameIndexKey($.userName.toLowerCase()));return Q.srem(this.idsSetKey(),Z),await Q.exec(this.redis),1}async list(Z){let $=R(Z),Q=(await this.redis.smembers(this.idsSetKey())).sort();if(Q.length===0)return D$(0,[],$.start??0,$.limit);let W=Q.map((G)=>this.entityKey(G)),V=(await this.redis.mget(W)).filter((G)=>G!=null).map((G)=>d(G,this.log)).filter((G)=>G!=null);return _(V,$)}}import{list as P$,StoreConflictError as A$}from"@ionite/core";var K$="workload-identity:ids",R$="workload-identity:client:";function E$(Z){let $=Z.createdAt??new Date,Q=Z.updatedAt??$;return JSON.stringify({...Z,createdAt:$.toISOString(),updatedAt:Q.toISOString()})}function n(Z,$){let Q=C(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 RZ{redis;prefix;log;constructor(Z){this.redis=Z.redis,this.prefix=F(Z,"workload-identity-store"),this.log=D(Z)}key(...Z){return X(this.prefix,...Z)}entityKey(Z){return this.key("workload-identity",H(Z))}idsSetKey(){return this.key(K$)}clientIndexKey(Z){return this.key(R$+H(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.clientId){let $=await this.redis.get(this.clientIndexKey(Z.clientId));if($!=null)return this.get($)}return}async upsert(Z){let $=Z.id?.trim()||crypto.randomUUID(),Q=this.entityKey($),W=await this.redis.get(Q),Y=null;if(W!=null)Y=n(W,this.log)??null;let V=new Date,G={...Z,id:$,createdAt:Y?.createdAt??Z.createdAt??V,updatedAt:V},q=new L;if(G.clientId)q.claimUnique(this.clientIndexKey(G.clientId),$);if(Y?.clientId&&Y.clientId!==G.clientId)q.del(this.clientIndexKey(Y.clientId));if(q.set(Q,E$(G)),G.clientId)q.set(this.clientIndexKey(G.clientId),$);if(q.sadd(this.idsSetKey(),$),await q.exec(this.redis)==="conflict")throw new A$(`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 L;if(Q.del(this.entityKey(Z)),$.clientId)Q.del(this.clientIndexKey($.clientId));return Q.srem(this.idsSetKey(),Z),await Q.exec(this.redis),1}async list(Z){let $=R(Z),Q=(await this.redis.smembers(this.idsSetKey())).sort();if(Q.length===0)return P$(0,[],$.start??0,$.limit);let W=Q.map((G)=>this.entityKey(G)),V=(await this.redis.mget(W)).filter((G)=>G!=null).map((G)=>n(G,this.log)).filter((G)=>G!=null);return _(V,$)}}function S$(Z){return JSON.stringify({...Z,expires_at:Z.expires_at.toISOString(),created_at:Z.created_at.toISOString()})}function f$(Z,$){let Q=C(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 EZ{redis;prefix;log;constructor(Z){this.redis=Z.redis,this.prefix=F(Z,"workload-token-store"),this.log=D(Z)}key(Z){return X(this.prefix,"workload","token",H(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,S$(Z))}async get(Z){let $=await this.redis.get(this.key(Z));if($==null)return null;let Q=f$($,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{A as supportsRunScript,G$ as splitIonRedisSecret,JZ as scriptSha1,X as prefixedKey,D as getLogger,F as getKeyPrefix,H as encodeKeyId,J$ as createReactiveRedis,W$ as createEvalshaRunner,oZ as chunkedMget,T as atomicCreate,g as atomicCompareAndSet,EZ as RedisWorkloadTokenStore,RZ as RedisWorkloadIdentityStore,KZ as RedisUserStore,L as RedisTxn,AZ as RedisTenantStore,_Z as RedisSessionStore,VZ as RedisResourceStore,OZ as RedisMagicLinkStore,MZ as RedisIamDeltaIdMapStore,HZ as RedisCustomerStore,UZ as RedisAccessIndex,GZ as RedisAccessCatalogStore,YZ as RedisAccessAssignmentStore,s as MAX_LIST_LIMIT,aZ as DEFAULT_MGET_CHUNK_SIZE,r as DEFAULT_LIST_LIMIT};
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@ionite/redis-core",
3
+ "version": "0.0.21",
4
+ "description": "Redis adapter interface and store implementations for ionite. Use with @ionite/redis (node-redis), @ionite/ioredis, or a custom RedisAdapter.",
5
+ "private": false,
6
+ "author": "ionite",
7
+ "license": "proprietary",
8
+ "type": "module",
9
+ "engines": {
10
+ "node": ">=20.6"
11
+ },
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "module": "./dist/index.js",
16
+ "types": "./dist/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "import": {
20
+ "types": "./dist/index.d.ts",
21
+ "default": "./dist/index.js"
22
+ }
23
+ },
24
+ "./package.json": "./package.json"
25
+ },
26
+ "dependencies": {
27
+ "@ionite/core": "^0.0.21"
28
+ },
29
+ "devDependencies": {
30
+ "@types/node": "^26.1.0"
31
+ }
32
+ }