@owlmeans/server-auth-session 0.1.18-rc.2 → 0.1.18-rc.20
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 +115 -13
- package/agent-meta/manifest.json +2 -2
- package/build/consts.local.d.ts +11 -0
- package/build/consts.local.d.ts.map +1 -0
- package/build/consts.local.js +44 -0
- package/build/consts.local.js.map +1 -0
- package/build/memory.d.ts +1 -1
- package/build/memory.d.ts.map +1 -1
- package/build/memory.js +1 -1
- package/build/memory.js.map +1 -1
- package/build/redis.d.ts +2 -2
- package/build/redis.d.ts.map +1 -1
- package/build/redis.js +1 -43
- package/build/redis.js.map +1 -1
- package/build/types.local.d.ts +4 -0
- package/build/types.local.d.ts.map +1 -0
- package/build/types.local.js +2 -0
- package/build/types.local.js.map +1 -0
- package/package.json +6 -6
- package/src/consts.local.ts +47 -0
- package/src/memory.ts +2 -4
- package/src/redis.ts +5 -56
- package/src/types.local.ts +4 -0
package/README.md
CHANGED
|
@@ -1,32 +1,134 @@
|
|
|
1
1
|
# @owlmeans/server-auth-session
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
An authorization-freshness registry for OwlMeans bearer and OIDC sessions: every issued session is
|
|
4
|
+
registered under an opaque id with an absolute seven-day lifetime, and a profile's authority can be
|
|
5
|
+
fenced, then refreshed or revoked, so the next bearer decision sees the change. It ships an
|
|
6
|
+
in-memory manager for a single process and an atomic Redis manager for multi-instance deployments.
|
|
7
|
+
A server uses it through `@owlmeans/server-auth`: `appendAuthService` already registers the memory
|
|
8
|
+
manager when none is present, so an app touches this package directly only to install the Redis
|
|
9
|
+
manager or to fence/refresh/revoke a profile after changing its permissions, role or status. It is
|
|
10
|
+
not a browser-token store and holds no claims — tokens and their payloads stay with
|
|
11
|
+
`@owlmeans/server-auth`.
|
|
6
12
|
|
|
7
13
|
## Installation
|
|
8
14
|
|
|
9
15
|
```bash
|
|
10
|
-
bun add @owlmeans/server-auth-session@^0.1.18-rc.
|
|
16
|
+
bun add @owlmeans/server-auth-session@^0.1.18-rc.20
|
|
11
17
|
```
|
|
12
18
|
|
|
19
|
+
## Concepts
|
|
20
|
+
|
|
21
|
+
- **Session** — an `AuthSessionRecord` addressed by an opaque `id`, of an `AuthSessionKind`
|
|
22
|
+
(`'bearer'`, `'oidc-access'`, `'oidc-refresh'`, `'oidc-provider-session'`), carrying the subject
|
|
23
|
+
version it was issued at and its absolute `expiresAt`.
|
|
24
|
+
- **Subject** — the stable selector `{ entityId, profileId, clientId? }` shared by all sessions of
|
|
25
|
+
one profile (per client). `entityId` is storage-only — never the renameable `entitySlug`, and never
|
|
26
|
+
placed in an authentication envelope.
|
|
27
|
+
- **Subject state** — `active`, `pending` (fenced while authority changes), `refresh` (claims
|
|
28
|
+
changed; re-issue required), `revoked`. Each completed transition increments the subject
|
|
29
|
+
`version`.
|
|
30
|
+
- **Decision** — `AuthSessionDecision`: `{ state: 'active' | 'refresh', version, expiresAt }` or
|
|
31
|
+
`{ state: 'pending' | 'revoked' | 'missing' | 'expired' }`.
|
|
32
|
+
- **Fence** — `fence(selector, operationId)` marks the subject pending; only `refresh` or `revoke`
|
|
33
|
+
with the same `operationId` completes it.
|
|
34
|
+
|
|
13
35
|
## Usage
|
|
14
36
|
|
|
15
|
-
|
|
16
|
-
|
|
37
|
+
### Use the Redis manager in a scaled deployment
|
|
38
|
+
|
|
39
|
+
Register it before `appendAuthService`; both append helpers are idempotent by alias, so the first
|
|
40
|
+
one registered wins.
|
|
17
41
|
|
|
18
42
|
```typescript
|
|
19
|
-
import { appendMemoryAuthSessionManager } from '@owlmeans/server-auth-session'
|
|
20
43
|
import { appendAuthService } from '@owlmeans/server-auth'
|
|
44
|
+
import { appendRedisAuthSessionManager } from '@owlmeans/server-auth-session'
|
|
21
45
|
|
|
22
|
-
|
|
46
|
+
appendRedisAuthSessionManager(context) // registers AUTH_SESSION_RESOURCE as a Redis resource when absent
|
|
23
47
|
appendAuthService(context)
|
|
24
48
|
```
|
|
25
49
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
50
|
+
`appendRedisAuthSessionManager(context, { resourceAlias?, dbAlias?, serviceAlias?, alias?, now? })`
|
|
51
|
+
passes `dbAlias` / `serviceAlias` to `makeRedisResource`.
|
|
52
|
+
|
|
53
|
+
### Change a profile's authority
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { AUTH_SESSION_MANAGER } from '@owlmeans/server-auth-session'
|
|
57
|
+
import type { AuthSessionManager } from '@owlmeans/server-auth-session'
|
|
58
|
+
|
|
59
|
+
const sessions = context.service<AuthSessionManager>(AUTH_SESSION_MANAGER)
|
|
60
|
+
const selector = { entityId, profileId, clientId }
|
|
61
|
+
const operationId = crypto.randomUUID()
|
|
62
|
+
|
|
63
|
+
await sessions.fence(selector, operationId) // every session of the subject now answers 'pending'
|
|
64
|
+
await profiles.update({ ...profile, role }) // the durable authority change
|
|
65
|
+
await sessions.refresh(selector, operationId) // sessions answer 'refresh' → claims are re-issued
|
|
66
|
+
// or, on disable/removal:
|
|
67
|
+
await sessions.revoke(selector, operationId) // sessions answer 'revoked'
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
If the write or the completion fails, leave the subject pending — it is refused until a matching
|
|
71
|
+
completion runs.
|
|
72
|
+
|
|
73
|
+
### Register and inspect a session
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
const issued = await sessions.register({ id: sessionId, kind: 'bearer', entityId, profileId, clientId })
|
|
77
|
+
// { state: 'active', version, expiresAt } — expiresAt never exceeds now + AUTH_SESSION_TTL
|
|
78
|
+
|
|
79
|
+
const decision = await sessions.inspect(sessionId)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Test with a controllable clock
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import { makeMemoryAuthSessionManager } from '@owlmeans/server-auth-session'
|
|
86
|
+
|
|
87
|
+
let now = 1_000
|
|
88
|
+
const manager = makeMemoryAuthSessionManager({ now: () => now })
|
|
89
|
+
await manager.init()
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## API
|
|
93
|
+
|
|
94
|
+
| Symbol | Kind | Purpose |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| `AuthSessionManager` | interface | `register(session)`, `inspect(id)`, `fence(selector, op)`, `refresh(selector, op) → version`, `revoke(selector, op) → version` |
|
|
97
|
+
| `makeMemoryAuthSessionManager(options?)` | function | process-local manager; state is lost on restart |
|
|
98
|
+
| `appendMemoryAuthSessionManager(context, options?)` | function | registers the memory manager unless the alias is taken |
|
|
99
|
+
| `makeRedisAuthSessionManager(resourceAlias?, options?)` | function | Redis manager; atomic `SET NX` registration and Lua subject transitions |
|
|
100
|
+
| `appendRedisAuthSessionManager(context, options?)` | function | registers the Redis resource (if absent) and the Redis manager (if absent) |
|
|
101
|
+
| `AUTH_SESSION_TTL` | const | `7 * 24 * 60 * 60 * 1000` ms — absolute maximum, never extended |
|
|
102
|
+
| `AUTH_SESSION_MANAGER` | const | `'auth-session-manager'` — service alias |
|
|
103
|
+
| `AUTH_SESSION_RESOURCE` | const | `'auth-session-record'` — Redis resource alias |
|
|
104
|
+
| `AuthSessionManagerOptions` | type | `{ alias?, now? }` |
|
|
105
|
+
| `AuthSessionSelector`, `RegisterAuthSession` | types | `{ entityId, profileId, clientId? }`; selector + `id`, `kind`, `issuedAt?`, `expiresAt?` |
|
|
106
|
+
| `AuthSessionRecord`, `AuthSessionSubject`, `AuthSessionStoredRecord` | types | stored shapes (`record: 'session' \| 'subject'`) |
|
|
107
|
+
| `AuthSessionKind`, `AuthSessionState`, `AuthSessionDecision` | types | see Concepts |
|
|
108
|
+
|
|
109
|
+
## Common pitfalls
|
|
110
|
+
|
|
111
|
+
- Registering the Redis manager after `appendAuthService` has no effect — the memory manager
|
|
112
|
+
already holds `AUTH_SESSION_MANAGER`.
|
|
113
|
+
- The memory manager forgets everything on restart; use it only where another authority decides
|
|
114
|
+
each request live, or in development and tests.
|
|
115
|
+
- `register` clamps a caller-supplied `expiresAt` to `AUTH_SESSION_TTL`; re-registering after a
|
|
116
|
+
claim refresh keeps the original absolute expiry.
|
|
117
|
+
- A `refresh` or `revoke` whose `operationId` differs from a pending fence does not complete it
|
|
118
|
+
(memory returns the unchanged version; Redis throws `auth-session:transition-conflict`).
|
|
119
|
+
- A registry read or write failure is surfaced by `@owlmeans/server-auth` as `AuthUnavailable` (503) —
|
|
120
|
+
never treat it as a missing session or a logout.
|
|
121
|
+
- Do not replace the Redis Lua transitions with a read-modify-save sequence; a concurrent fence
|
|
122
|
+
would be overwritten.
|
|
123
|
+
|
|
124
|
+
## Related packages
|
|
125
|
+
|
|
126
|
+
- [`@owlmeans/server-auth`](../server-auth) — issues sessions and consults this registry at every bearer decision
|
|
127
|
+
- [`@owlmeans/redis-resource`](../redis-resource) — the Redis resource behind the scaled manager
|
|
128
|
+
- [`@owlmeans/server-auth-identity`](../server-auth-identity) — identity profiles whose authority changes are fenced here
|
|
129
|
+
- [`@owlmeans/context`](../context) — `createService` and the service lifecycle
|
|
130
|
+
|
|
131
|
+
The `server-auth-session` skill states the fence/refresh/revoke rules for agents.
|
|
30
132
|
|
|
31
133
|
<!-- owlmeans:agent-guidance:start -->
|
|
32
134
|
## Agent guidance
|
|
@@ -36,7 +138,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
|
|
|
36
138
|
your project's skill store (`.agents/skills/`):
|
|
37
139
|
|
|
38
140
|
```sh
|
|
39
|
-
npx @owlmeans/agent-skills@^0.1.18-rc.
|
|
141
|
+
npx @owlmeans/agent-skills@^0.1.18-rc.54
|
|
40
142
|
```
|
|
41
143
|
|
|
42
144
|
The embedded files are version-matched to this package release. Do not edit them
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/server-auth-session",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-
|
|
4
|
+
"version": "0.1.18-rc.20",
|
|
5
|
+
"generatedAt": "2026-10-15T10:20:01.290Z",
|
|
6
6
|
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AuthSessionDecision } from './types.js';
|
|
2
|
+
export declare const missing: AuthSessionDecision;
|
|
3
|
+
/**
|
|
4
|
+
* One Redis script owns each subject transition. A read-modify-save sequence lets competing
|
|
5
|
+
* grant/revoke operations overwrite a pending fence; this compares the fence id and increments
|
|
6
|
+
* the version in the same Redis command instead.
|
|
7
|
+
*/
|
|
8
|
+
export declare const transitionSubject = "\nlocal current = redis.call('GET', KEYS[1])\nlocal subject = current and cjson.decode(current) or cjson.decode(ARGV[1])\nlocal now = tonumber(ARGV[2])\nlocal expires = tonumber(ARGV[3])\nlocal state = ARGV[4]\nlocal operation = ARGV[5]\nif tonumber(subject.expiresAt) <= now then\n subject = cjson.decode(ARGV[1])\nend\nif state == 'pending' then\n subject.state = 'pending'\n subject.operationId = operation\nelse\n if subject.state == 'pending' and subject.operationId ~= operation then\n return -1\n end\n subject.state = state\n subject.operationId = cjson.null\n subject.version = tonumber(subject.version) + 1\nend\nsubject.expiresAt = math.max(tonumber(subject.expiresAt), expires)\nredis.call('SET', KEYS[1], cjson.encode(subject), 'PXAT', subject.expiresAt)\nreturn tonumber(subject.version)\n";
|
|
9
|
+
/** Extend only the subject's expiry without overwriting a concurrent fence or revocation. */
|
|
10
|
+
export declare const extendSubjectExpiry = "\nlocal current = redis.call('GET', KEYS[1])\nif not current then return 0 end\nlocal subject = cjson.decode(current)\nlocal expires = tonumber(ARGV[1])\nif tonumber(subject.expiresAt) < expires then\n subject.expiresAt = expires\n redis.call('SET', KEYS[1], cjson.encode(subject), 'PXAT', subject.expiresAt)\nend\nreturn 1\n";
|
|
11
|
+
//# sourceMappingURL=consts.local.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consts.local.d.ts","sourceRoot":"","sources":["../src/consts.local.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAErD,eAAO,MAAM,OAAO,EAAE,mBAA0C,CAAA;AAEhE;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,mzBAwB7B,CAAA;AAED,6FAA6F;AAC7F,eAAO,MAAM,mBAAmB,4UAU/B,CAAA"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export const missing = { state: 'missing' };
|
|
2
|
+
/**
|
|
3
|
+
* One Redis script owns each subject transition. A read-modify-save sequence lets competing
|
|
4
|
+
* grant/revoke operations overwrite a pending fence; this compares the fence id and increments
|
|
5
|
+
* the version in the same Redis command instead.
|
|
6
|
+
*/
|
|
7
|
+
export const transitionSubject = `
|
|
8
|
+
local current = redis.call('GET', KEYS[1])
|
|
9
|
+
local subject = current and cjson.decode(current) or cjson.decode(ARGV[1])
|
|
10
|
+
local now = tonumber(ARGV[2])
|
|
11
|
+
local expires = tonumber(ARGV[3])
|
|
12
|
+
local state = ARGV[4]
|
|
13
|
+
local operation = ARGV[5]
|
|
14
|
+
if tonumber(subject.expiresAt) <= now then
|
|
15
|
+
subject = cjson.decode(ARGV[1])
|
|
16
|
+
end
|
|
17
|
+
if state == 'pending' then
|
|
18
|
+
subject.state = 'pending'
|
|
19
|
+
subject.operationId = operation
|
|
20
|
+
else
|
|
21
|
+
if subject.state == 'pending' and subject.operationId ~= operation then
|
|
22
|
+
return -1
|
|
23
|
+
end
|
|
24
|
+
subject.state = state
|
|
25
|
+
subject.operationId = cjson.null
|
|
26
|
+
subject.version = tonumber(subject.version) + 1
|
|
27
|
+
end
|
|
28
|
+
subject.expiresAt = math.max(tonumber(subject.expiresAt), expires)
|
|
29
|
+
redis.call('SET', KEYS[1], cjson.encode(subject), 'PXAT', subject.expiresAt)
|
|
30
|
+
return tonumber(subject.version)
|
|
31
|
+
`;
|
|
32
|
+
/** Extend only the subject's expiry without overwriting a concurrent fence or revocation. */
|
|
33
|
+
export const extendSubjectExpiry = `
|
|
34
|
+
local current = redis.call('GET', KEYS[1])
|
|
35
|
+
if not current then return 0 end
|
|
36
|
+
local subject = cjson.decode(current)
|
|
37
|
+
local expires = tonumber(ARGV[1])
|
|
38
|
+
if tonumber(subject.expiresAt) < expires then
|
|
39
|
+
subject.expiresAt = expires
|
|
40
|
+
redis.call('SET', KEYS[1], cjson.encode(subject), 'PXAT', subject.expiresAt)
|
|
41
|
+
end
|
|
42
|
+
return 1
|
|
43
|
+
`;
|
|
44
|
+
//# sourceMappingURL=consts.local.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consts.local.js","sourceRoot":"","sources":["../src/consts.local.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,OAAO,GAAwB,EAAE,KAAK,EAAE,SAAS,EAAE,CAAA;AAEhE;;;;GAIG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;CAwBhC,CAAA;AAED,6FAA6F;AAC7F,MAAM,CAAC,MAAM,mBAAmB,GAAG;;;;;;;;;;CAUlC,CAAA"}
|
package/build/memory.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type BasicContext } from '@owlmeans/context';
|
|
2
2
|
import type { AuthSessionManager, AuthSessionManagerOptions } from './types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Process-local implementation for development and generated targets. It deliberately loses
|
package/build/memory.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../src/memory.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../src/memory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAGpE,OAAO,KAAK,EACW,kBAAkB,EAAE,yBAAyB,EAEnE,MAAM,YAAY,CAAA;AAGnB;;;GAGG;AACH,eAAO,MAAM,4BAA4B,aAC9B,yBAAyB,KACjC,kBAiFF,CAAA;AAED,eAAO,MAAM,8BAA8B,GAAI,CAAC,SAAS,YAAY,CAAC,GAAG,CAAC,WAC/D,CAAC,YAAY,yBAAyB,KAC9C,CAIF,CAAA"}
|
package/build/memory.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createService } from '@owlmeans/context';
|
|
2
2
|
import { AUTH_SESSION_MANAGER, AUTH_SESSION_TTL } from './consts.js';
|
|
3
3
|
import { subjectId } from './identity.js';
|
|
4
|
-
|
|
4
|
+
import { missing } from './consts.local.js';
|
|
5
5
|
/**
|
|
6
6
|
* Process-local implementation for development and generated targets. It deliberately loses
|
|
7
7
|
* state on restart and is therefore never the authority for viable's centrally issued sessions.
|
package/build/memory.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory.js","sourceRoot":"","sources":["../src/memory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,
|
|
1
|
+
{"version":3,"file":"memory.js","sourceRoot":"","sources":["../src/memory.ts"],"names":[],"mappings":"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"}
|
package/build/redis.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import type
|
|
1
|
+
import { type BasicContext } from '@owlmeans/context';
|
|
2
|
+
import { type RedisResource } from '@owlmeans/redis-resource';
|
|
3
3
|
import type { AuthSessionManager, AuthSessionManagerOptions } from './types.js';
|
|
4
4
|
/** Redis-backed manager. Redis `create` is SET NX, so first session registration is atomic. */
|
|
5
5
|
export declare const makeRedisAuthSessionManager: (resourceAlias?: string, options?: AuthSessionManagerOptions) => AuthSessionManager;
|
package/build/redis.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redis.d.ts","sourceRoot":"","sources":["../src/redis.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"redis.d.ts","sourceRoot":"","sources":["../src/redis.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAA;AACpE,OAAO,EAAqB,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAIhF,OAAO,KAAK,EAAE,kBAAkB,EAAE,yBAAyB,EAAoE,MAAM,YAAY,CAAA;AAIjJ,+FAA+F;AAC/F,eAAO,MAAM,2BAA2B,mBACvB,MAAM,YAAmC,yBAAyB,KAChF,kBA4GF,CAAA;AAED,eAAO,MAAM,6BAA6B,GAAI,CAAC,SAAS,YAAY,CAAC,GAAG,CAAC,WAC9D,CAAC,YAAY,yBAAyB,GAAG;IAAE,aAAa,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,KACpH,CAQF,CAAA;AAED,YAAY,EAAE,aAAa,EAAE,CAAA"}
|
package/build/redis.js
CHANGED
|
@@ -3,49 +3,7 @@ import { makeRedisResource } from '@owlmeans/redis-resource';
|
|
|
3
3
|
import { RecordExists } from '@owlmeans/resource';
|
|
4
4
|
import { AUTH_SESSION_MANAGER, AUTH_SESSION_RESOURCE, AUTH_SESSION_TTL } from './consts.js';
|
|
5
5
|
import { subjectId } from './identity.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* One Redis script owns each subject transition. A read-modify-save sequence lets competing
|
|
9
|
-
* grant/revoke operations overwrite a pending fence; this compares the fence id and increments
|
|
10
|
-
* the version in the same Redis command instead.
|
|
11
|
-
*/
|
|
12
|
-
const transitionSubject = `
|
|
13
|
-
local current = redis.call('GET', KEYS[1])
|
|
14
|
-
local subject = current and cjson.decode(current) or cjson.decode(ARGV[1])
|
|
15
|
-
local now = tonumber(ARGV[2])
|
|
16
|
-
local expires = tonumber(ARGV[3])
|
|
17
|
-
local state = ARGV[4]
|
|
18
|
-
local operation = ARGV[5]
|
|
19
|
-
if tonumber(subject.expiresAt) <= now then
|
|
20
|
-
subject = cjson.decode(ARGV[1])
|
|
21
|
-
end
|
|
22
|
-
if state == 'pending' then
|
|
23
|
-
subject.state = 'pending'
|
|
24
|
-
subject.operationId = operation
|
|
25
|
-
else
|
|
26
|
-
if subject.state == 'pending' and subject.operationId ~= operation then
|
|
27
|
-
return -1
|
|
28
|
-
end
|
|
29
|
-
subject.state = state
|
|
30
|
-
subject.operationId = cjson.null
|
|
31
|
-
subject.version = tonumber(subject.version) + 1
|
|
32
|
-
end
|
|
33
|
-
subject.expiresAt = math.max(tonumber(subject.expiresAt), expires)
|
|
34
|
-
redis.call('SET', KEYS[1], cjson.encode(subject), 'PXAT', subject.expiresAt)
|
|
35
|
-
return tonumber(subject.version)
|
|
36
|
-
`;
|
|
37
|
-
/** Extend only the subject's expiry without overwriting a concurrent fence or revocation. */
|
|
38
|
-
const extendSubjectExpiry = `
|
|
39
|
-
local current = redis.call('GET', KEYS[1])
|
|
40
|
-
if not current then return 0 end
|
|
41
|
-
local subject = cjson.decode(current)
|
|
42
|
-
local expires = tonumber(ARGV[1])
|
|
43
|
-
if tonumber(subject.expiresAt) < expires then
|
|
44
|
-
subject.expiresAt = expires
|
|
45
|
-
redis.call('SET', KEYS[1], cjson.encode(subject), 'PXAT', subject.expiresAt)
|
|
46
|
-
end
|
|
47
|
-
return 1
|
|
48
|
-
`;
|
|
6
|
+
import { extendSubjectExpiry, missing, transitionSubject } from './consts.local.js';
|
|
49
7
|
/** Redis-backed manager. Redis `create` is SET NX, so first session registration is atomic. */
|
|
50
8
|
export const makeRedisAuthSessionManager = (resourceAlias = AUTH_SESSION_RESOURCE, options = {}) => {
|
|
51
9
|
const now = options.now ?? Date.now;
|
package/build/redis.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"redis.js","sourceRoot":"","sources":["../src/redis.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,
|
|
1
|
+
{"version":3,"file":"redis.js","sourceRoot":"","sources":["../src/redis.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.local.d.ts","sourceRoot":"","sources":["../src/types.local.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAC7D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAA;AAEzD,MAAM,MAAM,eAAe,GAAG,aAAa,CAAC,uBAAuB,CAAC,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.local.js","sourceRoot":"","sources":["../src/types.local.ts"],"names":[],"mappings":""}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/server-auth-session",
|
|
3
|
-
"version": "0.1.18-rc.
|
|
3
|
+
"version": "0.1.18-rc.20",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Short-lived OwlMeans session registry with memory and Redis implementations.",
|
|
6
6
|
"type": "module",
|
|
@@ -21,14 +21,14 @@
|
|
|
21
21
|
}
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@owlmeans/context": "^0.1.18-rc.
|
|
25
|
-
"@owlmeans/redis-resource": "^0.1.18-rc.
|
|
26
|
-
"@owlmeans/resource": "^0.1.18-rc.
|
|
27
|
-
"@owlmeans/server-context": "^0.1.18-rc.
|
|
24
|
+
"@owlmeans/context": "^0.1.18-rc.34",
|
|
25
|
+
"@owlmeans/redis-resource": "^0.1.18-rc.47",
|
|
26
|
+
"@owlmeans/resource": "^0.1.18-rc.40",
|
|
27
|
+
"@owlmeans/server-context": "^0.1.18-rc.44"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@owlmeans/dep-config": "workspace:*",
|
|
31
|
-
"@types/bun": "^1.4.
|
|
31
|
+
"@types/bun": "^1.4.2",
|
|
32
32
|
"typescript": "^7.0.2"
|
|
33
33
|
},
|
|
34
34
|
"publishConfig": {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { AuthSessionDecision } from './types.js'
|
|
2
|
+
|
|
3
|
+
export const missing: AuthSessionDecision = { state: 'missing' }
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* One Redis script owns each subject transition. A read-modify-save sequence lets competing
|
|
7
|
+
* grant/revoke operations overwrite a pending fence; this compares the fence id and increments
|
|
8
|
+
* the version in the same Redis command instead.
|
|
9
|
+
*/
|
|
10
|
+
export const transitionSubject = `
|
|
11
|
+
local current = redis.call('GET', KEYS[1])
|
|
12
|
+
local subject = current and cjson.decode(current) or cjson.decode(ARGV[1])
|
|
13
|
+
local now = tonumber(ARGV[2])
|
|
14
|
+
local expires = tonumber(ARGV[3])
|
|
15
|
+
local state = ARGV[4]
|
|
16
|
+
local operation = ARGV[5]
|
|
17
|
+
if tonumber(subject.expiresAt) <= now then
|
|
18
|
+
subject = cjson.decode(ARGV[1])
|
|
19
|
+
end
|
|
20
|
+
if state == 'pending' then
|
|
21
|
+
subject.state = 'pending'
|
|
22
|
+
subject.operationId = operation
|
|
23
|
+
else
|
|
24
|
+
if subject.state == 'pending' and subject.operationId ~= operation then
|
|
25
|
+
return -1
|
|
26
|
+
end
|
|
27
|
+
subject.state = state
|
|
28
|
+
subject.operationId = cjson.null
|
|
29
|
+
subject.version = tonumber(subject.version) + 1
|
|
30
|
+
end
|
|
31
|
+
subject.expiresAt = math.max(tonumber(subject.expiresAt), expires)
|
|
32
|
+
redis.call('SET', KEYS[1], cjson.encode(subject), 'PXAT', subject.expiresAt)
|
|
33
|
+
return tonumber(subject.version)
|
|
34
|
+
`
|
|
35
|
+
|
|
36
|
+
/** Extend only the subject's expiry without overwriting a concurrent fence or revocation. */
|
|
37
|
+
export const extendSubjectExpiry = `
|
|
38
|
+
local current = redis.call('GET', KEYS[1])
|
|
39
|
+
if not current then return 0 end
|
|
40
|
+
local subject = cjson.decode(current)
|
|
41
|
+
local expires = tonumber(ARGV[1])
|
|
42
|
+
if tonumber(subject.expiresAt) < expires then
|
|
43
|
+
subject.expiresAt = expires
|
|
44
|
+
redis.call('SET', KEYS[1], cjson.encode(subject), 'PXAT', subject.expiresAt)
|
|
45
|
+
end
|
|
46
|
+
return 1
|
|
47
|
+
`
|
package/src/memory.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import { createService } from '@owlmeans/context'
|
|
2
|
-
import type { BasicContext } from '@owlmeans/context'
|
|
1
|
+
import { createService, type BasicContext } from '@owlmeans/context'
|
|
3
2
|
import { AUTH_SESSION_MANAGER, AUTH_SESSION_TTL } from './consts.js'
|
|
4
3
|
import { subjectId } from './identity.js'
|
|
5
4
|
import type {
|
|
6
5
|
AuthSessionDecision, AuthSessionManager, AuthSessionManagerOptions, AuthSessionSelector,
|
|
7
6
|
AuthSessionSubject, RegisterAuthSession
|
|
8
7
|
} from './types.js'
|
|
9
|
-
|
|
10
|
-
const missing: AuthSessionDecision = { state: 'missing' }
|
|
8
|
+
import { missing } from './consts.local.js'
|
|
11
9
|
|
|
12
10
|
/**
|
|
13
11
|
* Process-local implementation for development and generated targets. It deliberately loses
|
package/src/redis.ts
CHANGED
|
@@ -1,62 +1,11 @@
|
|
|
1
|
-
import { createService } from '@owlmeans/context'
|
|
2
|
-
import type
|
|
3
|
-
import { makeRedisResource } from '@owlmeans/redis-resource'
|
|
4
|
-
import type { RedisResource } from '@owlmeans/redis-resource'
|
|
1
|
+
import { createService, type BasicContext } from '@owlmeans/context'
|
|
2
|
+
import { makeRedisResource, type RedisResource } from '@owlmeans/redis-resource'
|
|
5
3
|
import { RecordExists } from '@owlmeans/resource'
|
|
6
4
|
import { AUTH_SESSION_MANAGER, AUTH_SESSION_RESOURCE, AUTH_SESSION_TTL } from './consts.js'
|
|
7
5
|
import { subjectId } from './identity.js'
|
|
8
|
-
import type {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from './types.js'
|
|
12
|
-
|
|
13
|
-
const missing: AuthSessionDecision = { state: 'missing' }
|
|
14
|
-
|
|
15
|
-
type SessionResource = RedisResource<AuthSessionStoredRecord>
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* One Redis script owns each subject transition. A read-modify-save sequence lets competing
|
|
19
|
-
* grant/revoke operations overwrite a pending fence; this compares the fence id and increments
|
|
20
|
-
* the version in the same Redis command instead.
|
|
21
|
-
*/
|
|
22
|
-
const transitionSubject = `
|
|
23
|
-
local current = redis.call('GET', KEYS[1])
|
|
24
|
-
local subject = current and cjson.decode(current) or cjson.decode(ARGV[1])
|
|
25
|
-
local now = tonumber(ARGV[2])
|
|
26
|
-
local expires = tonumber(ARGV[3])
|
|
27
|
-
local state = ARGV[4]
|
|
28
|
-
local operation = ARGV[5]
|
|
29
|
-
if tonumber(subject.expiresAt) <= now then
|
|
30
|
-
subject = cjson.decode(ARGV[1])
|
|
31
|
-
end
|
|
32
|
-
if state == 'pending' then
|
|
33
|
-
subject.state = 'pending'
|
|
34
|
-
subject.operationId = operation
|
|
35
|
-
else
|
|
36
|
-
if subject.state == 'pending' and subject.operationId ~= operation then
|
|
37
|
-
return -1
|
|
38
|
-
end
|
|
39
|
-
subject.state = state
|
|
40
|
-
subject.operationId = cjson.null
|
|
41
|
-
subject.version = tonumber(subject.version) + 1
|
|
42
|
-
end
|
|
43
|
-
subject.expiresAt = math.max(tonumber(subject.expiresAt), expires)
|
|
44
|
-
redis.call('SET', KEYS[1], cjson.encode(subject), 'PXAT', subject.expiresAt)
|
|
45
|
-
return tonumber(subject.version)
|
|
46
|
-
`
|
|
47
|
-
|
|
48
|
-
/** Extend only the subject's expiry without overwriting a concurrent fence or revocation. */
|
|
49
|
-
const extendSubjectExpiry = `
|
|
50
|
-
local current = redis.call('GET', KEYS[1])
|
|
51
|
-
if not current then return 0 end
|
|
52
|
-
local subject = cjson.decode(current)
|
|
53
|
-
local expires = tonumber(ARGV[1])
|
|
54
|
-
if tonumber(subject.expiresAt) < expires then
|
|
55
|
-
subject.expiresAt = expires
|
|
56
|
-
redis.call('SET', KEYS[1], cjson.encode(subject), 'PXAT', subject.expiresAt)
|
|
57
|
-
end
|
|
58
|
-
return 1
|
|
59
|
-
`
|
|
6
|
+
import type { AuthSessionManager, AuthSessionManagerOptions, AuthSessionSelector, AuthSessionStoredRecord, AuthSessionSubject } from './types.js'
|
|
7
|
+
import { extendSubjectExpiry, missing, transitionSubject } from './consts.local.js'
|
|
8
|
+
import type { SessionResource } from './types.local.js'
|
|
60
9
|
|
|
61
10
|
/** Redis-backed manager. Redis `create` is SET NX, so first session registration is atomic. */
|
|
62
11
|
export const makeRedisAuthSessionManager = (
|