@aooth/auth 0.1.6 → 0.1.8

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.
@@ -1,4 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_payload = require("./payload-BJjvj8AH.cjs");
2
3
  let node_crypto = require("node:crypto");
3
4
  //#region src/atscript-db/index.ts
4
5
  /**
@@ -22,18 +23,7 @@ var CredentialStoreAtscriptDb = class {
22
23
  async persist(state, ttl) {
23
24
  const token = (0, node_crypto.randomUUID)();
24
25
  const expiresAt = typeof ttl === "number" ? Date.now() + ttl : state.expiresAt;
25
- const row = {
26
- token,
27
- userId: state.userId,
28
- issuedAt: state.issuedAt,
29
- expiresAt,
30
- kind: state.kind,
31
- claims: state.claims,
32
- metadata: state.metadata,
33
- parentCredentialId: state.parentCredentialId,
34
- rotatedAt: state.rotatedAt
35
- };
36
- await this.table.insertOne(row);
26
+ await this.table.insertOne(stateToRow(state, token, expiresAt));
37
27
  return token;
38
28
  }
39
29
  async retrieve(token) {
@@ -57,23 +47,21 @@ var CredentialStoreAtscriptDb = class {
57
47
  await this.revoke(token);
58
48
  return token;
59
49
  }
60
- const row = {
61
- token,
62
- userId: state.userId,
63
- issuedAt: state.issuedAt,
64
- expiresAt: state.expiresAt,
65
- kind: state.kind,
66
- claims: state.claims,
67
- metadata: state.metadata,
68
- parentCredentialId: state.parentCredentialId,
69
- rotatedAt: state.rotatedAt
70
- };
71
- await this.table.replaceOne(row);
50
+ await this.table.replaceOne(stateToRow(state, token, state.expiresAt));
72
51
  return token;
73
52
  }
74
53
  async revoke(token) {
75
54
  await this.table.deleteOne(token);
76
55
  }
56
+ async touch(token, at) {
57
+ const row = await this.table.findOne({ filter: { token } });
58
+ if (!row) return;
59
+ if (row.expiresAt <= Date.now()) return;
60
+ await this.table.replaceOne({
61
+ ...row,
62
+ lastSeenAt: at
63
+ });
64
+ }
77
65
  async revokeAllForUser(userId) {
78
66
  return (await this.table.deleteMany({ userId })).deletedCount;
79
67
  }
@@ -96,17 +84,40 @@ var CredentialStoreAtscriptDb = class {
96
84
  return out;
97
85
  }
98
86
  };
87
+ /**
88
+ * Build the persisted row from a credential state: typed payload columns first
89
+ * (so an envelope field wins a name clash), then the fixed envelope fields and
90
+ * the resolved `token` + `expiresAt`. Shared by `persist` and `update`, which
91
+ * differ only in the `expiresAt` they resolve.
92
+ */
93
+ function stateToRow(state, token, expiresAt) {
94
+ return {
95
+ ...require_payload.credentialPayloadOf(state),
96
+ token,
97
+ userId: state.userId,
98
+ issuedAt: state.issuedAt,
99
+ expiresAt,
100
+ kind: state.kind,
101
+ metadata: state.metadata,
102
+ parentCredentialId: state.parentCredentialId,
103
+ rotatedAt: state.rotatedAt,
104
+ sessionId: state.sessionId,
105
+ lastSeenAt: state.lastSeenAt
106
+ };
107
+ }
99
108
  function rowToState(row) {
100
109
  const state = {
110
+ ...require_payload.credentialPayloadOf(row),
101
111
  userId: row.userId,
102
112
  issuedAt: row.issuedAt,
103
113
  expiresAt: row.expiresAt
104
114
  };
105
- if (row.claims !== void 0) state.claims = row.claims;
106
115
  if (row.metadata !== void 0) state.metadata = row.metadata;
107
116
  if (row.kind === "access" || row.kind === "refresh") state.kind = row.kind;
108
117
  if (row.parentCredentialId !== void 0) state.parentCredentialId = row.parentCredentialId;
109
118
  if (row.rotatedAt !== void 0) state.rotatedAt = row.rotatedAt;
119
+ if (row.sessionId !== void 0) state.sessionId = row.sessionId;
120
+ if (row.lastSeenAt !== void 0) state.lastSeenAt = row.lastSeenAt;
110
121
  return state;
111
122
  }
112
123
  //#endregion
@@ -1,20 +1,23 @@
1
- import { a as CredentialState, t as CredentialStore } from "./store-untAtWQz.cjs";
1
+ import { a as CredentialState, t as CredentialStore } from "./store-BG6m6oSJ.cjs";
2
2
 
3
3
  //#region src/atscript-db/index.d.ts
4
4
  /**
5
5
  * Persisted row shape — mirrors `AoothAuthCredential` from
6
- * `./auth-credential.as`. Re-declared here as a plain TS interface so
7
- * consumers can use the adapter without running the atscript build (and so
8
- * `@aooth/auth` doesn't need to depend on `@atscript/typescript` at build
9
- * time). When you DO wire the `.as` model, the shapes match by construction.
6
+ * `./auth-credential.as`. Re-declared here as a plain TS type so consumers can
7
+ * use the adapter without running the atscript build (and so `@aooth/auth`
8
+ * doesn't need to depend on `@atscript/typescript` at build time). When you DO
9
+ * wire the `.as` model, the shapes match by construction.
10
+ *
11
+ * The consumer's typed payload `TPayload` (the root fields they add to their
12
+ * `extends AoothAuthCredential` model) is intersected flat — those become real
13
+ * typed columns, replacing the dropped free-form `claims` blob.
10
14
  */
11
- interface AuthCredentialRow<TClaims extends object = object> {
15
+ type AuthCredentialRow<TPayload extends object = object> = {
12
16
  token: string;
13
17
  userId: string;
14
18
  issuedAt: number;
15
19
  expiresAt: number;
16
20
  kind?: string;
17
- claims?: TClaims;
18
21
  metadata?: {
19
22
  ip?: string;
20
23
  userAgent?: string;
@@ -23,25 +26,27 @@ interface AuthCredentialRow<TClaims extends object = object> {
23
26
  };
24
27
  parentCredentialId?: string;
25
28
  rotatedAt?: number;
26
- }
29
+ sessionId?: string;
30
+ lastSeenAt?: number;
31
+ } & TPayload;
27
32
  /**
28
33
  * Structural surface of `AtscriptDbTable` covering exactly the methods this
29
34
  * adapter calls. Kept loose to avoid pulling `@atscript/db` types into the
30
35
  * `@aooth/auth` public surface — consumers pass `db.getTable(AoothAuthCredential)`
31
36
  * directly and TypeScript matches by-shape.
32
37
  */
33
- interface AuthCredentialTable<TClaims extends object = object> {
34
- insertOne(row: AuthCredentialRow<TClaims>): Promise<{
38
+ interface AuthCredentialTable<TPayload extends object = object> {
39
+ insertOne(row: AuthCredentialRow<TPayload>): Promise<{
35
40
  insertedId: unknown;
36
41
  }>;
37
42
  findOne(query: {
38
43
  filter: Record<string, unknown>;
39
- }): Promise<AuthCredentialRow<TClaims> | null>;
44
+ }): Promise<AuthCredentialRow<TPayload> | null>;
40
45
  findMany(query: {
41
46
  filter?: Record<string, unknown>;
42
47
  controls?: Record<string, unknown>;
43
- }): Promise<AuthCredentialRow<TClaims>[]>;
44
- replaceOne(row: AuthCredentialRow<TClaims>): Promise<{
48
+ }): Promise<AuthCredentialRow<TPayload>[]>;
49
+ replaceOne(row: AuthCredentialRow<TPayload>): Promise<{
45
50
  matchedCount: number;
46
51
  modifiedCount: number;
47
52
  }>;
@@ -52,8 +57,8 @@ interface AuthCredentialTable<TClaims extends object = object> {
52
57
  deletedCount: number;
53
58
  }>;
54
59
  }
55
- interface CredentialStoreAtscriptDbOptions<TClaims extends object> {
56
- table: AuthCredentialTable<TClaims>;
60
+ interface CredentialStoreAtscriptDbOptions<TPayload extends object> {
61
+ table: AuthCredentialTable<TPayload>;
57
62
  }
58
63
  /**
59
64
  * atscript-db-backed `CredentialStore`.
@@ -68,16 +73,17 @@ interface CredentialStoreAtscriptDbOptions<TClaims extends object> {
68
73
  * `.as` model). Custom tables must keep `token` as PK or override the
69
74
  * adapter.
70
75
  */
71
- declare class CredentialStoreAtscriptDb<TClaims extends object = object> implements CredentialStore<TClaims> {
76
+ declare class CredentialStoreAtscriptDb<TPayload extends object = object> implements CredentialStore<TPayload> {
72
77
  private readonly table;
73
- constructor(opts: CredentialStoreAtscriptDbOptions<TClaims>);
74
- persist(state: CredentialState<TClaims>, ttl?: number): Promise<string>;
75
- retrieve(token: string): Promise<CredentialState<TClaims> | null>;
76
- consume(token: string): Promise<CredentialState<TClaims> | null>;
77
- update(token: string, state: CredentialState<TClaims>): Promise<string>;
78
+ constructor(opts: CredentialStoreAtscriptDbOptions<TPayload>);
79
+ persist(state: CredentialState & TPayload, ttl?: number): Promise<string>;
80
+ retrieve(token: string): Promise<(CredentialState & TPayload) | null>;
81
+ consume(token: string): Promise<(CredentialState & TPayload) | null>;
82
+ update(token: string, state: CredentialState & TPayload): Promise<string>;
78
83
  revoke(token: string): Promise<void>;
84
+ touch(token: string, at: number): Promise<void>;
79
85
  revokeAllForUser(userId: string): Promise<number>;
80
- listForUser(userId: string): Promise<Array<CredentialState<TClaims> & {
86
+ listForUser(userId: string): Promise<Array<CredentialState & TPayload & {
81
87
  token: string;
82
88
  }>>;
83
89
  }
@@ -1,20 +1,23 @@
1
- import { a as CredentialState, t as CredentialStore } from "./store-B1t8KkfA.mjs";
1
+ import { a as CredentialState, t as CredentialStore } from "./store-BG6m6oSJ.mjs";
2
2
 
3
3
  //#region src/atscript-db/index.d.ts
4
4
  /**
5
5
  * Persisted row shape — mirrors `AoothAuthCredential` from
6
- * `./auth-credential.as`. Re-declared here as a plain TS interface so
7
- * consumers can use the adapter without running the atscript build (and so
8
- * `@aooth/auth` doesn't need to depend on `@atscript/typescript` at build
9
- * time). When you DO wire the `.as` model, the shapes match by construction.
6
+ * `./auth-credential.as`. Re-declared here as a plain TS type so consumers can
7
+ * use the adapter without running the atscript build (and so `@aooth/auth`
8
+ * doesn't need to depend on `@atscript/typescript` at build time). When you DO
9
+ * wire the `.as` model, the shapes match by construction.
10
+ *
11
+ * The consumer's typed payload `TPayload` (the root fields they add to their
12
+ * `extends AoothAuthCredential` model) is intersected flat — those become real
13
+ * typed columns, replacing the dropped free-form `claims` blob.
10
14
  */
11
- interface AuthCredentialRow<TClaims extends object = object> {
15
+ type AuthCredentialRow<TPayload extends object = object> = {
12
16
  token: string;
13
17
  userId: string;
14
18
  issuedAt: number;
15
19
  expiresAt: number;
16
20
  kind?: string;
17
- claims?: TClaims;
18
21
  metadata?: {
19
22
  ip?: string;
20
23
  userAgent?: string;
@@ -23,25 +26,27 @@ interface AuthCredentialRow<TClaims extends object = object> {
23
26
  };
24
27
  parentCredentialId?: string;
25
28
  rotatedAt?: number;
26
- }
29
+ sessionId?: string;
30
+ lastSeenAt?: number;
31
+ } & TPayload;
27
32
  /**
28
33
  * Structural surface of `AtscriptDbTable` covering exactly the methods this
29
34
  * adapter calls. Kept loose to avoid pulling `@atscript/db` types into the
30
35
  * `@aooth/auth` public surface — consumers pass `db.getTable(AoothAuthCredential)`
31
36
  * directly and TypeScript matches by-shape.
32
37
  */
33
- interface AuthCredentialTable<TClaims extends object = object> {
34
- insertOne(row: AuthCredentialRow<TClaims>): Promise<{
38
+ interface AuthCredentialTable<TPayload extends object = object> {
39
+ insertOne(row: AuthCredentialRow<TPayload>): Promise<{
35
40
  insertedId: unknown;
36
41
  }>;
37
42
  findOne(query: {
38
43
  filter: Record<string, unknown>;
39
- }): Promise<AuthCredentialRow<TClaims> | null>;
44
+ }): Promise<AuthCredentialRow<TPayload> | null>;
40
45
  findMany(query: {
41
46
  filter?: Record<string, unknown>;
42
47
  controls?: Record<string, unknown>;
43
- }): Promise<AuthCredentialRow<TClaims>[]>;
44
- replaceOne(row: AuthCredentialRow<TClaims>): Promise<{
48
+ }): Promise<AuthCredentialRow<TPayload>[]>;
49
+ replaceOne(row: AuthCredentialRow<TPayload>): Promise<{
45
50
  matchedCount: number;
46
51
  modifiedCount: number;
47
52
  }>;
@@ -52,8 +57,8 @@ interface AuthCredentialTable<TClaims extends object = object> {
52
57
  deletedCount: number;
53
58
  }>;
54
59
  }
55
- interface CredentialStoreAtscriptDbOptions<TClaims extends object> {
56
- table: AuthCredentialTable<TClaims>;
60
+ interface CredentialStoreAtscriptDbOptions<TPayload extends object> {
61
+ table: AuthCredentialTable<TPayload>;
57
62
  }
58
63
  /**
59
64
  * atscript-db-backed `CredentialStore`.
@@ -68,16 +73,17 @@ interface CredentialStoreAtscriptDbOptions<TClaims extends object> {
68
73
  * `.as` model). Custom tables must keep `token` as PK or override the
69
74
  * adapter.
70
75
  */
71
- declare class CredentialStoreAtscriptDb<TClaims extends object = object> implements CredentialStore<TClaims> {
76
+ declare class CredentialStoreAtscriptDb<TPayload extends object = object> implements CredentialStore<TPayload> {
72
77
  private readonly table;
73
- constructor(opts: CredentialStoreAtscriptDbOptions<TClaims>);
74
- persist(state: CredentialState<TClaims>, ttl?: number): Promise<string>;
75
- retrieve(token: string): Promise<CredentialState<TClaims> | null>;
76
- consume(token: string): Promise<CredentialState<TClaims> | null>;
77
- update(token: string, state: CredentialState<TClaims>): Promise<string>;
78
+ constructor(opts: CredentialStoreAtscriptDbOptions<TPayload>);
79
+ persist(state: CredentialState & TPayload, ttl?: number): Promise<string>;
80
+ retrieve(token: string): Promise<(CredentialState & TPayload) | null>;
81
+ consume(token: string): Promise<(CredentialState & TPayload) | null>;
82
+ update(token: string, state: CredentialState & TPayload): Promise<string>;
78
83
  revoke(token: string): Promise<void>;
84
+ touch(token: string, at: number): Promise<void>;
79
85
  revokeAllForUser(userId: string): Promise<number>;
80
- listForUser(userId: string): Promise<Array<CredentialState<TClaims> & {
86
+ listForUser(userId: string): Promise<Array<CredentialState & TPayload & {
81
87
  token: string;
82
88
  }>>;
83
89
  }
@@ -1,3 +1,4 @@
1
+ import { t as credentialPayloadOf } from "./payload-D-DzH5-J.mjs";
1
2
  import { randomUUID } from "node:crypto";
2
3
  //#region src/atscript-db/index.ts
3
4
  /**
@@ -21,18 +22,7 @@ var CredentialStoreAtscriptDb = class {
21
22
  async persist(state, ttl) {
22
23
  const token = randomUUID();
23
24
  const expiresAt = typeof ttl === "number" ? Date.now() + ttl : state.expiresAt;
24
- const row = {
25
- token,
26
- userId: state.userId,
27
- issuedAt: state.issuedAt,
28
- expiresAt,
29
- kind: state.kind,
30
- claims: state.claims,
31
- metadata: state.metadata,
32
- parentCredentialId: state.parentCredentialId,
33
- rotatedAt: state.rotatedAt
34
- };
35
- await this.table.insertOne(row);
25
+ await this.table.insertOne(stateToRow(state, token, expiresAt));
36
26
  return token;
37
27
  }
38
28
  async retrieve(token) {
@@ -56,23 +46,21 @@ var CredentialStoreAtscriptDb = class {
56
46
  await this.revoke(token);
57
47
  return token;
58
48
  }
59
- const row = {
60
- token,
61
- userId: state.userId,
62
- issuedAt: state.issuedAt,
63
- expiresAt: state.expiresAt,
64
- kind: state.kind,
65
- claims: state.claims,
66
- metadata: state.metadata,
67
- parentCredentialId: state.parentCredentialId,
68
- rotatedAt: state.rotatedAt
69
- };
70
- await this.table.replaceOne(row);
49
+ await this.table.replaceOne(stateToRow(state, token, state.expiresAt));
71
50
  return token;
72
51
  }
73
52
  async revoke(token) {
74
53
  await this.table.deleteOne(token);
75
54
  }
55
+ async touch(token, at) {
56
+ const row = await this.table.findOne({ filter: { token } });
57
+ if (!row) return;
58
+ if (row.expiresAt <= Date.now()) return;
59
+ await this.table.replaceOne({
60
+ ...row,
61
+ lastSeenAt: at
62
+ });
63
+ }
76
64
  async revokeAllForUser(userId) {
77
65
  return (await this.table.deleteMany({ userId })).deletedCount;
78
66
  }
@@ -95,17 +83,40 @@ var CredentialStoreAtscriptDb = class {
95
83
  return out;
96
84
  }
97
85
  };
86
+ /**
87
+ * Build the persisted row from a credential state: typed payload columns first
88
+ * (so an envelope field wins a name clash), then the fixed envelope fields and
89
+ * the resolved `token` + `expiresAt`. Shared by `persist` and `update`, which
90
+ * differ only in the `expiresAt` they resolve.
91
+ */
92
+ function stateToRow(state, token, expiresAt) {
93
+ return {
94
+ ...credentialPayloadOf(state),
95
+ token,
96
+ userId: state.userId,
97
+ issuedAt: state.issuedAt,
98
+ expiresAt,
99
+ kind: state.kind,
100
+ metadata: state.metadata,
101
+ parentCredentialId: state.parentCredentialId,
102
+ rotatedAt: state.rotatedAt,
103
+ sessionId: state.sessionId,
104
+ lastSeenAt: state.lastSeenAt
105
+ };
106
+ }
98
107
  function rowToState(row) {
99
108
  const state = {
109
+ ...credentialPayloadOf(row),
100
110
  userId: row.userId,
101
111
  issuedAt: row.issuedAt,
102
112
  expiresAt: row.expiresAt
103
113
  };
104
- if (row.claims !== void 0) state.claims = row.claims;
105
114
  if (row.metadata !== void 0) state.metadata = row.metadata;
106
115
  if (row.kind === "access" || row.kind === "refresh") state.kind = row.kind;
107
116
  if (row.parentCredentialId !== void 0) state.parentCredentialId = row.parentCredentialId;
108
117
  if (row.rotatedAt !== void 0) state.rotatedAt = row.rotatedAt;
118
+ if (row.sessionId !== void 0) state.sessionId = row.sessionId;
119
+ if (row.lastSeenAt !== void 0) state.lastSeenAt = row.lastSeenAt;
109
120
  return state;
110
121
  }
111
122
  //#endregion
package/dist/authz.cjs ADDED
@@ -0,0 +1,168 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_clock = require("./clock-Bl-H3eqE.cjs");
3
+ let node_crypto = require("node:crypto");
4
+ //#region src/authz/authz-errors.ts
5
+ /** A typed authorization-server failure. */
6
+ var AuthorizeError = class extends Error {
7
+ code;
8
+ constructor(code, message) {
9
+ super(message);
10
+ this.name = "AuthorizeError";
11
+ this.code = code;
12
+ }
13
+ };
14
+ //#endregion
15
+ //#region src/authz/client-policy.ts
16
+ /**
17
+ * `true` when `uri` is a syntactically valid http(s) URL whose host is a
18
+ * **loopback literal** — `127.0.0.1`, `::1`, or `localhost` — on any port (RFC
19
+ * 8252 §7.3). Rejects everything else, including the classic bypasses: a
20
+ * host-suffix (`127.0.0.1.evil.com`, `localhost.evil.com`), embedded credentials
21
+ * (`http://127.0.0.1@evil.com` → host `evil.com`), a non-http scheme, and a bare
22
+ * `0.0.0.0`. Only a local process can receive a loopback redirect, which is why
23
+ * an arbitrary port is safe — the binding is the loopback host + PKCE.
24
+ */
25
+ function isLoopbackRedirectUri(uri) {
26
+ let url;
27
+ try {
28
+ url = new URL(uri);
29
+ } catch {
30
+ return false;
31
+ }
32
+ if (url.protocol !== "http:" && url.protocol !== "https:") return false;
33
+ if (url.username !== "" || url.password !== "") return false;
34
+ const host = url.hostname.replace(/^\[|\]$/g, "");
35
+ return host === "127.0.0.1" || host === "::1" || host === "localhost";
36
+ }
37
+ const DEFAULT_CLI_TOKEN_POLICY = {
38
+ kind: "cli-session",
39
+ ttl: 720 * 60 * 6e4
40
+ };
41
+ /**
42
+ * Tier-1 policy: accept any **loopback** `redirect_uri`, treat the client as a
43
+ * public client (no `client_id` / secret — PKCE is the binding), and mint the
44
+ * configured CLI token policy. Rejects every non-loopback redirect.
45
+ */
46
+ var LoopbackClientPolicy = class {
47
+ tokenPolicy;
48
+ constructor(opts) {
49
+ this.tokenPolicy = opts?.tokenPolicy ?? DEFAULT_CLI_TOKEN_POLICY;
50
+ }
51
+ resolveClient(args) {
52
+ if (!isLoopbackRedirectUri(args.redirectUri)) throw new AuthorizeError("invalid_redirect", "redirect_uri must be a loopback address");
53
+ return {
54
+ redirectUri: args.redirectUri,
55
+ tokenPolicy: structuredClone(this.tokenPolicy)
56
+ };
57
+ }
58
+ };
59
+ //#endregion
60
+ //#region src/authz/pending-authorization-store.ts
61
+ /**
62
+ * Storage seam for in-flight authorizations (AUTH-SERVER.md §4.3). Short-lived
63
+ * (≈ the login-session ceiling): created at `/authorize`, read+deleted at the
64
+ * wf terminal. An in-memory impl ships for single-process apps + tests; a
65
+ * multi-pod deployment provides a durable (e.g. Redis) impl under the same
66
+ * `PENDING_AUTHORIZATION_STORE_TOKEN` (from `@aooth/auth-moost`).
67
+ */
68
+ var PendingAuthorizationStore = class {};
69
+ const DEFAULT_PENDING_TTL_MS = 15 * 6e4;
70
+ /**
71
+ * In-memory {@link PendingAuthorizationStore} — the reference impl for a
72
+ * single-process app + tests. `structuredClone` on read/write isolates callers.
73
+ */
74
+ var PendingAuthorizationStoreMemory = class extends PendingAuthorizationStore {
75
+ store = /* @__PURE__ */ new Map();
76
+ clock;
77
+ ttlMs;
78
+ constructor(opts) {
79
+ super();
80
+ this.clock = opts?.clock ?? require_clock.defaultClock;
81
+ this.ttlMs = opts?.ttlMs ?? DEFAULT_PENDING_TTL_MS;
82
+ }
83
+ async create(rec) {
84
+ const now = this.clock.now();
85
+ const row = {
86
+ handle: (0, node_crypto.randomUUID)(),
87
+ redirectUri: rec.redirectUri,
88
+ codeChallenge: rec.codeChallenge,
89
+ tokenPolicy: structuredClone(rec.tokenPolicy),
90
+ createdAt: now,
91
+ expiresAt: now + this.ttlMs,
92
+ ...rec.clientId !== void 0 && { clientId: rec.clientId },
93
+ ...rec.clientState !== void 0 && { clientState: rec.clientState },
94
+ ...rec.scope !== void 0 && { scope: rec.scope }
95
+ };
96
+ this.store.set(row.handle, structuredClone(row));
97
+ return { handle: row.handle };
98
+ }
99
+ async get(handle) {
100
+ const row = this.store.get(handle);
101
+ if (!row) return null;
102
+ if (row.expiresAt <= this.clock.now()) {
103
+ this.store.delete(handle);
104
+ return null;
105
+ }
106
+ return structuredClone(row);
107
+ }
108
+ async delete(handle) {
109
+ return this.store.delete(handle);
110
+ }
111
+ };
112
+ //#endregion
113
+ //#region src/authz/auth-code-store.ts
114
+ /**
115
+ * Storage seam for issued authorization codes (AUTH-SERVER.md §4.3). Very
116
+ * short-lived (≈ 30–60 s) and **single-use**: {@link consume} returns the row
117
+ * AND invalidates it in one atomic step, so a concurrent double-redeem (or a
118
+ * back-button replay) yields the code to exactly one caller. An in-memory impl
119
+ * ships (atomic for free in single-threaded JS); a durable impl must implement
120
+ * `consume` as an atomic check-and-delete (e.g. `withCas` / `@db.column.version`,
121
+ * or a Redis `GETDEL`).
122
+ */
123
+ var AuthCodeStore = class {};
124
+ const DEFAULT_CODE_TTL_MS = 6e4;
125
+ /**
126
+ * In-memory {@link AuthCodeStore} — the reference impl. `consume` is atomic
127
+ * because the `get` + `delete` run with no intervening `await`, so a second
128
+ * concurrent `consume` of the same code always misses.
129
+ */
130
+ var AuthCodeStoreMemory = class extends AuthCodeStore {
131
+ store = /* @__PURE__ */ new Map();
132
+ clock;
133
+ ttlMs;
134
+ constructor(opts) {
135
+ super();
136
+ this.clock = opts?.clock ?? require_clock.defaultClock;
137
+ this.ttlMs = opts?.ttlMs ?? DEFAULT_CODE_TTL_MS;
138
+ }
139
+ async mint(rec) {
140
+ const code = (0, node_crypto.randomUUID)();
141
+ const row = {
142
+ code,
143
+ userId: rec.userId,
144
+ codeChallenge: rec.codeChallenge,
145
+ redirectUri: rec.redirectUri,
146
+ tokenPolicy: structuredClone(rec.tokenPolicy),
147
+ expiresAt: this.clock.now() + this.ttlMs,
148
+ ...rec.clientId !== void 0 && { clientId: rec.clientId }
149
+ };
150
+ this.store.set(code, structuredClone(row));
151
+ return { code };
152
+ }
153
+ async consume(code) {
154
+ const row = this.store.get(code);
155
+ if (!row) return null;
156
+ this.store.delete(code);
157
+ if (row.expiresAt <= this.clock.now()) return null;
158
+ return structuredClone(row);
159
+ }
160
+ };
161
+ //#endregion
162
+ exports.AuthCodeStore = AuthCodeStore;
163
+ exports.AuthCodeStoreMemory = AuthCodeStoreMemory;
164
+ exports.AuthorizeError = AuthorizeError;
165
+ exports.LoopbackClientPolicy = LoopbackClientPolicy;
166
+ exports.PendingAuthorizationStore = PendingAuthorizationStore;
167
+ exports.PendingAuthorizationStoreMemory = PendingAuthorizationStoreMemory;
168
+ exports.isLoopbackRedirectUri = isLoopbackRedirectUri;