@aooth/auth 0.1.10 → 0.1.11

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.
@@ -0,0 +1,151 @@
1
+ const require_clock = require("./clock-Bl-H3eqE.cjs");
2
+ let node_crypto = require("node:crypto");
3
+ //#region src/authz/pending-authorization-store.ts
4
+ /**
5
+ * Storage seam for in-flight authorizations (AUTH-SERVER.md §4.3). Short-lived
6
+ * (≈ the login-session ceiling): created at `/authorize`, read+deleted at the
7
+ * wf terminal. An in-memory impl ships for single-process apps + tests; a
8
+ * multi-pod deployment provides a durable (e.g. Redis) impl under the same
9
+ * `PENDING_AUTHORIZATION_STORE_TOKEN` (from `@aooth/auth-moost`).
10
+ */
11
+ var PendingAuthorizationStore = class {};
12
+ /** Default pending-authorization lifetime (15 min). Shared by the memory + atscript-db stores. */
13
+ const DEFAULT_PENDING_TTL_MS = 15 * 6e4;
14
+ /**
15
+ * In-memory {@link PendingAuthorizationStore} — the reference impl for a
16
+ * single-process app + tests. `structuredClone` on read/write isolates callers.
17
+ */
18
+ var PendingAuthorizationStoreMemory = class extends PendingAuthorizationStore {
19
+ store = /* @__PURE__ */ new Map();
20
+ clock;
21
+ ttlMs;
22
+ constructor(opts) {
23
+ super();
24
+ this.clock = opts?.clock ?? require_clock.defaultClock;
25
+ this.ttlMs = opts?.ttlMs ?? 9e5;
26
+ }
27
+ async create(rec) {
28
+ const now = this.clock.now();
29
+ const row = {
30
+ handle: (0, node_crypto.randomUUID)(),
31
+ redirectUri: rec.redirectUri,
32
+ codeChallenge: rec.codeChallenge,
33
+ tokenPolicy: structuredClone(rec.tokenPolicy),
34
+ createdAt: now,
35
+ expiresAt: now + this.ttlMs,
36
+ ...rec.clientId !== void 0 && { clientId: rec.clientId },
37
+ ...rec.clientState !== void 0 && { clientState: rec.clientState },
38
+ ...rec.scope !== void 0 && { scope: rec.scope },
39
+ ...rec.nonce !== void 0 && { nonce: rec.nonce },
40
+ ...rec.idToken !== void 0 && { idToken: rec.idToken },
41
+ ...rec.accessToken !== void 0 && { accessToken: rec.accessToken },
42
+ ...rec.audience !== void 0 && { audience: rec.audience }
43
+ };
44
+ this.store.set(row.handle, structuredClone(row));
45
+ return { handle: row.handle };
46
+ }
47
+ async get(handle) {
48
+ const row = this.store.get(handle);
49
+ if (!row) return null;
50
+ if (row.expiresAt <= this.clock.now()) {
51
+ this.store.delete(handle);
52
+ return null;
53
+ }
54
+ return structuredClone(row);
55
+ }
56
+ async delete(handle) {
57
+ return this.store.delete(handle);
58
+ }
59
+ };
60
+ //#endregion
61
+ //#region src/authz/auth-code-store.ts
62
+ /**
63
+ * Storage seam for issued authorization codes (AUTH-SERVER.md §4.3). Very
64
+ * short-lived (≈ 30–60 s) and **single-use**: {@link consume} returns the row
65
+ * AND invalidates it in one atomic step, so a concurrent double-redeem (or a
66
+ * back-button replay) yields the code to exactly one caller. An in-memory impl
67
+ * ships (atomic for free in single-threaded JS); a durable impl must implement
68
+ * `consume` as an atomic check-and-delete (e.g. `withCas` / `@db.column.version`,
69
+ * or a Redis `GETDEL`).
70
+ */
71
+ var AuthCodeStore = class {};
72
+ /** Default auth-code lifetime (60 s). Shared by the memory + atscript-db stores. */
73
+ const DEFAULT_CODE_TTL_MS = 6e4;
74
+ /**
75
+ * In-memory {@link AuthCodeStore} — the reference impl. `consume` is atomic
76
+ * because the `get` + `delete` run with no intervening `await`, so a second
77
+ * concurrent `consume` of the same code always misses.
78
+ */
79
+ var AuthCodeStoreMemory = class extends AuthCodeStore {
80
+ store = /* @__PURE__ */ new Map();
81
+ clock;
82
+ ttlMs;
83
+ constructor(opts) {
84
+ super();
85
+ this.clock = opts?.clock ?? require_clock.defaultClock;
86
+ this.ttlMs = opts?.ttlMs ?? 6e4;
87
+ }
88
+ async mint(rec) {
89
+ const code = (0, node_crypto.randomUUID)();
90
+ const row = {
91
+ code,
92
+ userId: rec.userId,
93
+ codeChallenge: rec.codeChallenge,
94
+ redirectUri: rec.redirectUri,
95
+ tokenPolicy: structuredClone(rec.tokenPolicy),
96
+ expiresAt: this.clock.now() + this.ttlMs,
97
+ ...rec.clientId !== void 0 && { clientId: rec.clientId },
98
+ ...rec.scope !== void 0 && { scope: rec.scope },
99
+ ...rec.nonce !== void 0 && { nonce: rec.nonce },
100
+ ...rec.idToken !== void 0 && { idToken: rec.idToken },
101
+ ...rec.accessToken !== void 0 && { accessToken: rec.accessToken },
102
+ ...rec.audience !== void 0 && { audience: rec.audience }
103
+ };
104
+ this.store.set(code, structuredClone(row));
105
+ return { code };
106
+ }
107
+ async consume(code) {
108
+ const row = this.store.get(code);
109
+ if (!row) return null;
110
+ this.store.delete(code);
111
+ if (row.expiresAt <= this.clock.now()) return null;
112
+ return structuredClone(row);
113
+ }
114
+ };
115
+ //#endregion
116
+ Object.defineProperty(exports, "AuthCodeStore", {
117
+ enumerable: true,
118
+ get: function() {
119
+ return AuthCodeStore;
120
+ }
121
+ });
122
+ Object.defineProperty(exports, "AuthCodeStoreMemory", {
123
+ enumerable: true,
124
+ get: function() {
125
+ return AuthCodeStoreMemory;
126
+ }
127
+ });
128
+ Object.defineProperty(exports, "DEFAULT_CODE_TTL_MS", {
129
+ enumerable: true,
130
+ get: function() {
131
+ return DEFAULT_CODE_TTL_MS;
132
+ }
133
+ });
134
+ Object.defineProperty(exports, "DEFAULT_PENDING_TTL_MS", {
135
+ enumerable: true,
136
+ get: function() {
137
+ return DEFAULT_PENDING_TTL_MS;
138
+ }
139
+ });
140
+ Object.defineProperty(exports, "PendingAuthorizationStore", {
141
+ enumerable: true,
142
+ get: function() {
143
+ return PendingAuthorizationStore;
144
+ }
145
+ });
146
+ Object.defineProperty(exports, "PendingAuthorizationStoreMemory", {
147
+ enumerable: true,
148
+ get: function() {
149
+ return PendingAuthorizationStoreMemory;
150
+ }
151
+ });
@@ -0,0 +1,199 @@
1
+ import { t as Clock } from "./clock-BjXa0LXb.cjs";
2
+
3
+ //#region src/authz/token-policy.d.ts
4
+ /**
5
+ * What the authorization server mints for a completed grant — forwarded verbatim
6
+ * into `AuthCredential.issue(userId, …)` at the token endpoint. Decided by the
7
+ * {@link import("./client-policy").ClientRedirectPolicy} (per client / scope),
8
+ * never by the client request, and recorded on the pending-authorization + the
9
+ * issued auth-code so the grant's authority is fixed at `/authorize` time, not
10
+ * `/token` time.
11
+ *
12
+ * Tier 1 (CLI / loopback) → a full-authority `cli-session` (`{ kind, ttl }`, no
13
+ * `payload`). A scoped service token (Tier 2) additionally sets `payload` with
14
+ * the consumer's attenuation root fields.
15
+ */
16
+ interface TokenPolicy {
17
+ /** Semantic credential kind stamped on the token (e.g. `"cli-session"`). See `IssueOptions.kind`. */
18
+ kind?: string;
19
+ /** Per-mint access-token lifetime in ms (forwarded to `IssueOptions.ttl`). */
20
+ ttl?: number;
21
+ /**
22
+ * Extra root payload merged into `issue()` — e.g. `@arbac.attenuate.*` fields
23
+ * for a SCOPED token. Omit for a full-authority token. MUST be JSON-safe: it
24
+ * is persisted on the pending-authorization + auth-code records.
25
+ */
26
+ payload?: Record<string, unknown>;
27
+ }
28
+ //#endregion
29
+ //#region src/authz/pending-authorization-store.d.ts
30
+ /**
31
+ * One in-flight authorization request, recorded at `GET /auth/authorize` and
32
+ * read once at the login-workflow terminal that mints the auth code. Keyed by an
33
+ * opaque `handle` that rides the login-wf ctx (and, across a "Continue with
34
+ * Google" detour, the federated signed `state`) — so nothing secret leaves the
35
+ * server.
36
+ */
37
+ interface PendingAuthorization {
38
+ /** Opaque server-side handle (the only thing that rides the URL / wf state). */
39
+ handle: string;
40
+ /** Registered client id (Tier 2), absent for a public/loopback client. */
41
+ clientId?: string;
42
+ /** The client's validated `redirect_uri` — where the code is delivered. */
43
+ redirectUri: string;
44
+ /** PKCE S256 challenge (client-generated); verified against the verifier at `/token`. */
45
+ codeChallenge: string;
46
+ /** The client's `state`, echoed back on the redirect so the client can correlate. */
47
+ clientState?: string;
48
+ /** Granted scope (space-joined) — `requested ∩ allowed`; drives the `id_token` profile claims. */
49
+ scope?: string;
50
+ /** OIDC `nonce` from the authorize request — echoed into the `id_token` (Tier 2). */
51
+ nonce?: string;
52
+ /** Mint an `id_token` at `/token` (Tier 2). */
53
+ idToken?: boolean;
54
+ /** Mint an access token at `/token`. Omitted ⇒ minted (Tier-1 loopback). */
55
+ accessToken?: boolean;
56
+ /** The `id_token` `aud` (the registered `client_id`). */
57
+ audience?: string;
58
+ /** What the grant will mint (fixed at authorize time). */
59
+ tokenPolicy: TokenPolicy;
60
+ createdAt: number;
61
+ expiresAt: number;
62
+ }
63
+ /** Input to {@link PendingAuthorizationStore.create} — `handle`/timestamps are store-assigned. */
64
+ interface NewPendingAuthorization {
65
+ clientId?: string;
66
+ redirectUri: string;
67
+ codeChallenge: string;
68
+ clientState?: string;
69
+ scope?: string;
70
+ nonce?: string;
71
+ idToken?: boolean;
72
+ accessToken?: boolean;
73
+ audience?: string;
74
+ tokenPolicy: TokenPolicy;
75
+ }
76
+ /**
77
+ * Storage seam for in-flight authorizations (AUTH-SERVER.md §4.3). Short-lived
78
+ * (≈ the login-session ceiling): created at `/authorize`, read+deleted at the
79
+ * wf terminal. An in-memory impl ships for single-process apps + tests; a
80
+ * multi-pod deployment provides a durable (e.g. Redis) impl under the same
81
+ * `PENDING_AUTHORIZATION_STORE_TOKEN` (from `@aooth/auth-moost`).
82
+ */
83
+ declare abstract class PendingAuthorizationStore {
84
+ /** Record a new pending authorization; returns its opaque `handle`. */
85
+ abstract create(rec: NewPendingAuthorization): Promise<{
86
+ handle: string;
87
+ }>;
88
+ /** Fetch by handle, or `null` when unknown/expired. */
89
+ abstract get(handle: string): Promise<PendingAuthorization | null>;
90
+ /** Drop a handle once consumed. Returns `true` when a row was removed. */
91
+ abstract delete(handle: string): Promise<boolean>;
92
+ }
93
+ interface PendingAuthorizationStoreMemoryOptions {
94
+ /** Injectable clock for deterministic expiry. Defaults to {@link defaultClock}. */
95
+ clock?: Clock;
96
+ /** How long a pending authorization stays valid. Default 15 min. */
97
+ ttlMs?: number;
98
+ }
99
+ /**
100
+ * In-memory {@link PendingAuthorizationStore} — the reference impl for a
101
+ * single-process app + tests. `structuredClone` on read/write isolates callers.
102
+ */
103
+ declare class PendingAuthorizationStoreMemory extends PendingAuthorizationStore {
104
+ private store;
105
+ private clock;
106
+ private ttlMs;
107
+ constructor(opts?: PendingAuthorizationStoreMemoryOptions);
108
+ create(rec: NewPendingAuthorization): Promise<{
109
+ handle: string;
110
+ }>;
111
+ get(handle: string): Promise<PendingAuthorization | null>;
112
+ delete(handle: string): Promise<boolean>;
113
+ }
114
+ //#endregion
115
+ //#region src/authz/auth-code-store.d.ts
116
+ /**
117
+ * A minted, single-use authorization code, bound to the user the login resolved
118
+ * plus the PKCE challenge / redirect / token policy recorded at `/authorize`.
119
+ * Consumed atomically at `POST /auth/token` — the token is minted there, off the
120
+ * browser, so nothing long-lived ever rides a redirect URL.
121
+ */
122
+ interface AuthCode {
123
+ /** Opaque single-use code (delivered to the client in the redirect query). */
124
+ code: string;
125
+ /** The user the login workflow authenticated. */
126
+ userId: string;
127
+ /** PKCE S256 challenge from the originating authorize request. */
128
+ codeChallenge: string;
129
+ /** The client's `redirect_uri` (bound; the code is meaningless elsewhere). */
130
+ redirectUri: string;
131
+ /** Registered client id (Tier 2), absent for a public/loopback client. */
132
+ clientId?: string;
133
+ /** Granted scope (space-joined) — drives the `id_token` profile claims. */
134
+ scope?: string;
135
+ /** OIDC `nonce` from the authorize request — echoed into the `id_token` (Tier 2). */
136
+ nonce?: string;
137
+ /** Mint an `id_token` at `/token` (Tier 2). */
138
+ idToken?: boolean;
139
+ /** Mint an access token at `/token`. Omitted ⇒ minted (Tier-1 loopback). */
140
+ accessToken?: boolean;
141
+ /** The `id_token` `aud` (the registered `client_id`). */
142
+ audience?: string;
143
+ /** What `/token` mints when this code is redeemed. */
144
+ tokenPolicy: TokenPolicy;
145
+ expiresAt: number;
146
+ }
147
+ /** Input to {@link AuthCodeStore.mint} — `code`/`expiresAt` are store-assigned. */
148
+ interface NewAuthCode {
149
+ userId: string;
150
+ codeChallenge: string;
151
+ redirectUri: string;
152
+ clientId?: string;
153
+ scope?: string;
154
+ nonce?: string;
155
+ idToken?: boolean;
156
+ accessToken?: boolean;
157
+ audience?: string;
158
+ tokenPolicy: TokenPolicy;
159
+ }
160
+ /**
161
+ * Storage seam for issued authorization codes (AUTH-SERVER.md §4.3). Very
162
+ * short-lived (≈ 30–60 s) and **single-use**: {@link consume} returns the row
163
+ * AND invalidates it in one atomic step, so a concurrent double-redeem (or a
164
+ * back-button replay) yields the code to exactly one caller. An in-memory impl
165
+ * ships (atomic for free in single-threaded JS); a durable impl must implement
166
+ * `consume` as an atomic check-and-delete (e.g. `withCas` / `@db.column.version`,
167
+ * or a Redis `GETDEL`).
168
+ */
169
+ declare abstract class AuthCodeStore {
170
+ /** Mint + store a single-use code; returns the opaque code string. */
171
+ abstract mint(rec: NewAuthCode): Promise<{
172
+ code: string;
173
+ }>;
174
+ /** Atomically claim + return the code's row, or `null` on miss / reuse / expiry. */
175
+ abstract consume(code: string): Promise<AuthCode | null>;
176
+ }
177
+ interface AuthCodeStoreMemoryOptions {
178
+ /** Injectable clock for deterministic expiry. Defaults to {@link defaultClock}. */
179
+ clock?: Clock;
180
+ /** Code lifetime. Default 60 s. */
181
+ ttlMs?: number;
182
+ }
183
+ /**
184
+ * In-memory {@link AuthCodeStore} — the reference impl. `consume` is atomic
185
+ * because the `get` + `delete` run with no intervening `await`, so a second
186
+ * concurrent `consume` of the same code always misses.
187
+ */
188
+ declare class AuthCodeStoreMemory extends AuthCodeStore {
189
+ private store;
190
+ private clock;
191
+ private ttlMs;
192
+ constructor(opts?: AuthCodeStoreMemoryOptions);
193
+ mint(rec: NewAuthCode): Promise<{
194
+ code: string;
195
+ }>;
196
+ consume(code: string): Promise<AuthCode | null>;
197
+ }
198
+ //#endregion
199
+ export { NewAuthCode as a, PendingAuthorizationStore as c, TokenPolicy as d, AuthCodeStoreMemoryOptions as i, PendingAuthorizationStoreMemory as l, AuthCodeStore as n, NewPendingAuthorization as o, AuthCodeStoreMemory as r, PendingAuthorization as s, AuthCode as t, PendingAuthorizationStoreMemoryOptions as u };
@@ -0,0 +1,199 @@
1
+ import { t as Clock } from "./clock-BjXa0LXb.mjs";
2
+
3
+ //#region src/authz/token-policy.d.ts
4
+ /**
5
+ * What the authorization server mints for a completed grant — forwarded verbatim
6
+ * into `AuthCredential.issue(userId, …)` at the token endpoint. Decided by the
7
+ * {@link import("./client-policy").ClientRedirectPolicy} (per client / scope),
8
+ * never by the client request, and recorded on the pending-authorization + the
9
+ * issued auth-code so the grant's authority is fixed at `/authorize` time, not
10
+ * `/token` time.
11
+ *
12
+ * Tier 1 (CLI / loopback) → a full-authority `cli-session` (`{ kind, ttl }`, no
13
+ * `payload`). A scoped service token (Tier 2) additionally sets `payload` with
14
+ * the consumer's attenuation root fields.
15
+ */
16
+ interface TokenPolicy {
17
+ /** Semantic credential kind stamped on the token (e.g. `"cli-session"`). See `IssueOptions.kind`. */
18
+ kind?: string;
19
+ /** Per-mint access-token lifetime in ms (forwarded to `IssueOptions.ttl`). */
20
+ ttl?: number;
21
+ /**
22
+ * Extra root payload merged into `issue()` — e.g. `@arbac.attenuate.*` fields
23
+ * for a SCOPED token. Omit for a full-authority token. MUST be JSON-safe: it
24
+ * is persisted on the pending-authorization + auth-code records.
25
+ */
26
+ payload?: Record<string, unknown>;
27
+ }
28
+ //#endregion
29
+ //#region src/authz/pending-authorization-store.d.ts
30
+ /**
31
+ * One in-flight authorization request, recorded at `GET /auth/authorize` and
32
+ * read once at the login-workflow terminal that mints the auth code. Keyed by an
33
+ * opaque `handle` that rides the login-wf ctx (and, across a "Continue with
34
+ * Google" detour, the federated signed `state`) — so nothing secret leaves the
35
+ * server.
36
+ */
37
+ interface PendingAuthorization {
38
+ /** Opaque server-side handle (the only thing that rides the URL / wf state). */
39
+ handle: string;
40
+ /** Registered client id (Tier 2), absent for a public/loopback client. */
41
+ clientId?: string;
42
+ /** The client's validated `redirect_uri` — where the code is delivered. */
43
+ redirectUri: string;
44
+ /** PKCE S256 challenge (client-generated); verified against the verifier at `/token`. */
45
+ codeChallenge: string;
46
+ /** The client's `state`, echoed back on the redirect so the client can correlate. */
47
+ clientState?: string;
48
+ /** Granted scope (space-joined) — `requested ∩ allowed`; drives the `id_token` profile claims. */
49
+ scope?: string;
50
+ /** OIDC `nonce` from the authorize request — echoed into the `id_token` (Tier 2). */
51
+ nonce?: string;
52
+ /** Mint an `id_token` at `/token` (Tier 2). */
53
+ idToken?: boolean;
54
+ /** Mint an access token at `/token`. Omitted ⇒ minted (Tier-1 loopback). */
55
+ accessToken?: boolean;
56
+ /** The `id_token` `aud` (the registered `client_id`). */
57
+ audience?: string;
58
+ /** What the grant will mint (fixed at authorize time). */
59
+ tokenPolicy: TokenPolicy;
60
+ createdAt: number;
61
+ expiresAt: number;
62
+ }
63
+ /** Input to {@link PendingAuthorizationStore.create} — `handle`/timestamps are store-assigned. */
64
+ interface NewPendingAuthorization {
65
+ clientId?: string;
66
+ redirectUri: string;
67
+ codeChallenge: string;
68
+ clientState?: string;
69
+ scope?: string;
70
+ nonce?: string;
71
+ idToken?: boolean;
72
+ accessToken?: boolean;
73
+ audience?: string;
74
+ tokenPolicy: TokenPolicy;
75
+ }
76
+ /**
77
+ * Storage seam for in-flight authorizations (AUTH-SERVER.md §4.3). Short-lived
78
+ * (≈ the login-session ceiling): created at `/authorize`, read+deleted at the
79
+ * wf terminal. An in-memory impl ships for single-process apps + tests; a
80
+ * multi-pod deployment provides a durable (e.g. Redis) impl under the same
81
+ * `PENDING_AUTHORIZATION_STORE_TOKEN` (from `@aooth/auth-moost`).
82
+ */
83
+ declare abstract class PendingAuthorizationStore {
84
+ /** Record a new pending authorization; returns its opaque `handle`. */
85
+ abstract create(rec: NewPendingAuthorization): Promise<{
86
+ handle: string;
87
+ }>;
88
+ /** Fetch by handle, or `null` when unknown/expired. */
89
+ abstract get(handle: string): Promise<PendingAuthorization | null>;
90
+ /** Drop a handle once consumed. Returns `true` when a row was removed. */
91
+ abstract delete(handle: string): Promise<boolean>;
92
+ }
93
+ interface PendingAuthorizationStoreMemoryOptions {
94
+ /** Injectable clock for deterministic expiry. Defaults to {@link defaultClock}. */
95
+ clock?: Clock;
96
+ /** How long a pending authorization stays valid. Default 15 min. */
97
+ ttlMs?: number;
98
+ }
99
+ /**
100
+ * In-memory {@link PendingAuthorizationStore} — the reference impl for a
101
+ * single-process app + tests. `structuredClone` on read/write isolates callers.
102
+ */
103
+ declare class PendingAuthorizationStoreMemory extends PendingAuthorizationStore {
104
+ private store;
105
+ private clock;
106
+ private ttlMs;
107
+ constructor(opts?: PendingAuthorizationStoreMemoryOptions);
108
+ create(rec: NewPendingAuthorization): Promise<{
109
+ handle: string;
110
+ }>;
111
+ get(handle: string): Promise<PendingAuthorization | null>;
112
+ delete(handle: string): Promise<boolean>;
113
+ }
114
+ //#endregion
115
+ //#region src/authz/auth-code-store.d.ts
116
+ /**
117
+ * A minted, single-use authorization code, bound to the user the login resolved
118
+ * plus the PKCE challenge / redirect / token policy recorded at `/authorize`.
119
+ * Consumed atomically at `POST /auth/token` — the token is minted there, off the
120
+ * browser, so nothing long-lived ever rides a redirect URL.
121
+ */
122
+ interface AuthCode {
123
+ /** Opaque single-use code (delivered to the client in the redirect query). */
124
+ code: string;
125
+ /** The user the login workflow authenticated. */
126
+ userId: string;
127
+ /** PKCE S256 challenge from the originating authorize request. */
128
+ codeChallenge: string;
129
+ /** The client's `redirect_uri` (bound; the code is meaningless elsewhere). */
130
+ redirectUri: string;
131
+ /** Registered client id (Tier 2), absent for a public/loopback client. */
132
+ clientId?: string;
133
+ /** Granted scope (space-joined) — drives the `id_token` profile claims. */
134
+ scope?: string;
135
+ /** OIDC `nonce` from the authorize request — echoed into the `id_token` (Tier 2). */
136
+ nonce?: string;
137
+ /** Mint an `id_token` at `/token` (Tier 2). */
138
+ idToken?: boolean;
139
+ /** Mint an access token at `/token`. Omitted ⇒ minted (Tier-1 loopback). */
140
+ accessToken?: boolean;
141
+ /** The `id_token` `aud` (the registered `client_id`). */
142
+ audience?: string;
143
+ /** What `/token` mints when this code is redeemed. */
144
+ tokenPolicy: TokenPolicy;
145
+ expiresAt: number;
146
+ }
147
+ /** Input to {@link AuthCodeStore.mint} — `code`/`expiresAt` are store-assigned. */
148
+ interface NewAuthCode {
149
+ userId: string;
150
+ codeChallenge: string;
151
+ redirectUri: string;
152
+ clientId?: string;
153
+ scope?: string;
154
+ nonce?: string;
155
+ idToken?: boolean;
156
+ accessToken?: boolean;
157
+ audience?: string;
158
+ tokenPolicy: TokenPolicy;
159
+ }
160
+ /**
161
+ * Storage seam for issued authorization codes (AUTH-SERVER.md §4.3). Very
162
+ * short-lived (≈ 30–60 s) and **single-use**: {@link consume} returns the row
163
+ * AND invalidates it in one atomic step, so a concurrent double-redeem (or a
164
+ * back-button replay) yields the code to exactly one caller. An in-memory impl
165
+ * ships (atomic for free in single-threaded JS); a durable impl must implement
166
+ * `consume` as an atomic check-and-delete (e.g. `withCas` / `@db.column.version`,
167
+ * or a Redis `GETDEL`).
168
+ */
169
+ declare abstract class AuthCodeStore {
170
+ /** Mint + store a single-use code; returns the opaque code string. */
171
+ abstract mint(rec: NewAuthCode): Promise<{
172
+ code: string;
173
+ }>;
174
+ /** Atomically claim + return the code's row, or `null` on miss / reuse / expiry. */
175
+ abstract consume(code: string): Promise<AuthCode | null>;
176
+ }
177
+ interface AuthCodeStoreMemoryOptions {
178
+ /** Injectable clock for deterministic expiry. Defaults to {@link defaultClock}. */
179
+ clock?: Clock;
180
+ /** Code lifetime. Default 60 s. */
181
+ ttlMs?: number;
182
+ }
183
+ /**
184
+ * In-memory {@link AuthCodeStore} — the reference impl. `consume` is atomic
185
+ * because the `get` + `delete` run with no intervening `await`, so a second
186
+ * concurrent `consume` of the same code always misses.
187
+ */
188
+ declare class AuthCodeStoreMemory extends AuthCodeStore {
189
+ private store;
190
+ private clock;
191
+ private ttlMs;
192
+ constructor(opts?: AuthCodeStoreMemoryOptions);
193
+ mint(rec: NewAuthCode): Promise<{
194
+ code: string;
195
+ }>;
196
+ consume(code: string): Promise<AuthCode | null>;
197
+ }
198
+ //#endregion
199
+ export { NewAuthCode as a, PendingAuthorizationStore as c, TokenPolicy as d, AuthCodeStoreMemoryOptions as i, PendingAuthorizationStoreMemory as l, AuthCodeStore as n, NewPendingAuthorization as o, AuthCodeStoreMemory as r, PendingAuthorization as s, AuthCode as t, PendingAuthorizationStoreMemoryOptions as u };
@@ -0,0 +1,116 @@
1
+ import { t as defaultClock } from "./clock-Bdsep_1j.mjs";
2
+ import { randomUUID } from "node:crypto";
3
+ //#region src/authz/pending-authorization-store.ts
4
+ /**
5
+ * Storage seam for in-flight authorizations (AUTH-SERVER.md §4.3). Short-lived
6
+ * (≈ the login-session ceiling): created at `/authorize`, read+deleted at the
7
+ * wf terminal. An in-memory impl ships for single-process apps + tests; a
8
+ * multi-pod deployment provides a durable (e.g. Redis) impl under the same
9
+ * `PENDING_AUTHORIZATION_STORE_TOKEN` (from `@aooth/auth-moost`).
10
+ */
11
+ var PendingAuthorizationStore = class {};
12
+ /** Default pending-authorization lifetime (15 min). Shared by the memory + atscript-db stores. */
13
+ const DEFAULT_PENDING_TTL_MS = 15 * 6e4;
14
+ /**
15
+ * In-memory {@link PendingAuthorizationStore} — the reference impl for a
16
+ * single-process app + tests. `structuredClone` on read/write isolates callers.
17
+ */
18
+ var PendingAuthorizationStoreMemory = class extends PendingAuthorizationStore {
19
+ store = /* @__PURE__ */ new Map();
20
+ clock;
21
+ ttlMs;
22
+ constructor(opts) {
23
+ super();
24
+ this.clock = opts?.clock ?? defaultClock;
25
+ this.ttlMs = opts?.ttlMs ?? 9e5;
26
+ }
27
+ async create(rec) {
28
+ const now = this.clock.now();
29
+ const row = {
30
+ handle: randomUUID(),
31
+ redirectUri: rec.redirectUri,
32
+ codeChallenge: rec.codeChallenge,
33
+ tokenPolicy: structuredClone(rec.tokenPolicy),
34
+ createdAt: now,
35
+ expiresAt: now + this.ttlMs,
36
+ ...rec.clientId !== void 0 && { clientId: rec.clientId },
37
+ ...rec.clientState !== void 0 && { clientState: rec.clientState },
38
+ ...rec.scope !== void 0 && { scope: rec.scope },
39
+ ...rec.nonce !== void 0 && { nonce: rec.nonce },
40
+ ...rec.idToken !== void 0 && { idToken: rec.idToken },
41
+ ...rec.accessToken !== void 0 && { accessToken: rec.accessToken },
42
+ ...rec.audience !== void 0 && { audience: rec.audience }
43
+ };
44
+ this.store.set(row.handle, structuredClone(row));
45
+ return { handle: row.handle };
46
+ }
47
+ async get(handle) {
48
+ const row = this.store.get(handle);
49
+ if (!row) return null;
50
+ if (row.expiresAt <= this.clock.now()) {
51
+ this.store.delete(handle);
52
+ return null;
53
+ }
54
+ return structuredClone(row);
55
+ }
56
+ async delete(handle) {
57
+ return this.store.delete(handle);
58
+ }
59
+ };
60
+ //#endregion
61
+ //#region src/authz/auth-code-store.ts
62
+ /**
63
+ * Storage seam for issued authorization codes (AUTH-SERVER.md §4.3). Very
64
+ * short-lived (≈ 30–60 s) and **single-use**: {@link consume} returns the row
65
+ * AND invalidates it in one atomic step, so a concurrent double-redeem (or a
66
+ * back-button replay) yields the code to exactly one caller. An in-memory impl
67
+ * ships (atomic for free in single-threaded JS); a durable impl must implement
68
+ * `consume` as an atomic check-and-delete (e.g. `withCas` / `@db.column.version`,
69
+ * or a Redis `GETDEL`).
70
+ */
71
+ var AuthCodeStore = class {};
72
+ /** Default auth-code lifetime (60 s). Shared by the memory + atscript-db stores. */
73
+ const DEFAULT_CODE_TTL_MS = 6e4;
74
+ /**
75
+ * In-memory {@link AuthCodeStore} — the reference impl. `consume` is atomic
76
+ * because the `get` + `delete` run with no intervening `await`, so a second
77
+ * concurrent `consume` of the same code always misses.
78
+ */
79
+ var AuthCodeStoreMemory = class extends AuthCodeStore {
80
+ store = /* @__PURE__ */ new Map();
81
+ clock;
82
+ ttlMs;
83
+ constructor(opts) {
84
+ super();
85
+ this.clock = opts?.clock ?? defaultClock;
86
+ this.ttlMs = opts?.ttlMs ?? 6e4;
87
+ }
88
+ async mint(rec) {
89
+ const code = randomUUID();
90
+ const row = {
91
+ code,
92
+ userId: rec.userId,
93
+ codeChallenge: rec.codeChallenge,
94
+ redirectUri: rec.redirectUri,
95
+ tokenPolicy: structuredClone(rec.tokenPolicy),
96
+ expiresAt: this.clock.now() + this.ttlMs,
97
+ ...rec.clientId !== void 0 && { clientId: rec.clientId },
98
+ ...rec.scope !== void 0 && { scope: rec.scope },
99
+ ...rec.nonce !== void 0 && { nonce: rec.nonce },
100
+ ...rec.idToken !== void 0 && { idToken: rec.idToken },
101
+ ...rec.accessToken !== void 0 && { accessToken: rec.accessToken },
102
+ ...rec.audience !== void 0 && { audience: rec.audience }
103
+ };
104
+ this.store.set(code, structuredClone(row));
105
+ return { code };
106
+ }
107
+ async consume(code) {
108
+ const row = this.store.get(code);
109
+ if (!row) return null;
110
+ this.store.delete(code);
111
+ if (row.expiresAt <= this.clock.now()) return null;
112
+ return structuredClone(row);
113
+ }
114
+ };
115
+ //#endregion
116
+ export { PendingAuthorizationStore as a, DEFAULT_PENDING_TTL_MS as i, AuthCodeStoreMemory as n, PendingAuthorizationStoreMemory as o, DEFAULT_CODE_TTL_MS as r, AuthCodeStore as t };