@cogenta/auth 0.3.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/api-keys.d.ts +93 -4
  2. package/dist/api-keys.d.ts.map +1 -1
  3. package/dist/api-keys.js +260 -27
  4. package/dist/api-keys.js.map +1 -1
  5. package/dist/audit-integrity.d.ts +61 -0
  6. package/dist/audit-integrity.d.ts.map +1 -0
  7. package/dist/audit-integrity.js +130 -0
  8. package/dist/audit-integrity.js.map +1 -0
  9. package/dist/audit.d.ts +78 -2
  10. package/dist/audit.d.ts.map +1 -1
  11. package/dist/audit.js +168 -24
  12. package/dist/audit.js.map +1 -1
  13. package/dist/credentials.d.ts +27 -0
  14. package/dist/credentials.d.ts.map +1 -1
  15. package/dist/credentials.js +61 -0
  16. package/dist/credentials.js.map +1 -1
  17. package/dist/index.d.ts +11 -6
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +5 -2
  20. package/dist/index.js.map +1 -1
  21. package/dist/login.d.ts +63 -6
  22. package/dist/login.d.ts.map +1 -1
  23. package/dist/login.js +90 -10
  24. package/dist/login.js.map +1 -1
  25. package/dist/mfa.d.ts +1 -1
  26. package/dist/mfa.d.ts.map +1 -1
  27. package/dist/mfa.js +3 -1
  28. package/dist/mfa.js.map +1 -1
  29. package/dist/recovery-codes.d.ts +25 -0
  30. package/dist/recovery-codes.d.ts.map +1 -0
  31. package/dist/recovery-codes.js +56 -0
  32. package/dist/recovery-codes.js.map +1 -0
  33. package/dist/resets.d.ts +15 -1
  34. package/dist/resets.d.ts.map +1 -1
  35. package/dist/resets.js +10 -0
  36. package/dist/resets.js.map +1 -1
  37. package/dist/sessions.d.ts +24 -0
  38. package/dist/sessions.d.ts.map +1 -1
  39. package/dist/sessions.js +25 -4
  40. package/dist/sessions.js.map +1 -1
  41. package/dist/store.d.ts +3 -0
  42. package/dist/store.d.ts.map +1 -1
  43. package/dist/store.js +4 -1
  44. package/dist/store.js.map +1 -1
  45. package/dist/tables.d.ts +17 -0
  46. package/dist/tables.d.ts.map +1 -1
  47. package/dist/tables.js +129 -1
  48. package/dist/tables.js.map +1 -1
  49. package/dist/types.d.ts +89 -2
  50. package/dist/types.d.ts.map +1 -1
  51. package/dist/types.js +10 -1
  52. package/dist/types.js.map +1 -1
  53. package/dist/user-agent.d.ts +18 -0
  54. package/dist/user-agent.d.ts.map +1 -0
  55. package/dist/user-agent.js +39 -0
  56. package/dist/user-agent.js.map +1 -0
  57. package/dist/users.d.ts +24 -1
  58. package/dist/users.d.ts.map +1 -1
  59. package/dist/users.js +72 -3
  60. package/dist/users.js.map +1 -1
  61. package/package.json +3 -3
@@ -1,19 +1,108 @@
1
1
  import { type DatabaseHandle } from '@cogenta/core';
2
- import type { ApiKey, CreateApiKeyInput, IssuedApiKey } from './types.js';
2
+ import type { ApiKey, ApiKeyRotationResult, ApiKeyUsage, CreateApiKeyInput, IssuedApiKey } from './types.js';
3
+ /**
4
+ * A generous but real per-key request quota (fiche 20, "décisions à
5
+ * prendre"): high enough that no legitimate integration ever notices it,
6
+ * low enough that a leaked key cannot read the whole site as fast as the
7
+ * network allows. Applied whenever a key is minted — by `create` or by
8
+ * `rotate` — without an explicit quota of its own.
9
+ */
10
+ export declare const DEFAULT_RATE_LIMIT_PER_MINUTE = 600;
11
+ /**
12
+ * Fiche 62 task 2: how long a revoked key has to sit before it can be
13
+ * purged — the same reasoning and the same default as content's own
14
+ * `DEFAULT_TRASH_RETAIN_DAYS` (`@cogenta/schema`), applied here because a
15
+ * revoked key is not stored anywhere else: purging too early is the one
16
+ * mistake `purge()` must never make, since it is a real `DELETE`, not a
17
+ * second layer of soft state on top of `revoked_at`.
18
+ */
19
+ export declare const MIN_PURGE_AFTER_REVOKED_DAYS = 30;
20
+ /**
21
+ * Fiche 62 task 3 (decision "(b)", recommended by the fiche): a key revoked
22
+ * by mistake is not reactivated — `revoked_at` never goes back to `null` —
23
+ * but a short window after the mistake, `recover()` mints a replacement the
24
+ * same way `rotate()` does for an active key. Past this window the only way
25
+ * back is minting a brand new key from scratch, on purpose: the longer a key
26
+ * has been dead, the less likely "revoked by mistake" is the true story.
27
+ */
28
+ export declare const RECOVERY_WINDOW_MS: number;
3
29
  /** A raw bearer token is an API key only if it has this shape — checked before any DB lookup. */
4
30
  export declare function looksLikeApiKey(token: string): boolean;
31
+ export interface RotateApiKeyOptions {
32
+ /** How long the replaced key keeps working alongside the new one. */
33
+ readonly graceMs: number;
34
+ }
35
+ /**
36
+ * Fiche 67 task 5. Both omitted (the historical call shape, `mcp.tsx`'s
37
+ * picker among others) still returns every key, unpaginated, byte for byte —
38
+ * this is additive, not a breaking change to an existing publishable
39
+ * interface.
40
+ */
41
+ export interface ListApiKeysOptions {
42
+ readonly limit?: number;
43
+ readonly offset?: number;
44
+ }
45
+ /**
46
+ * Fiche feedback: a key's name/scope/quota had no edit path at all after
47
+ * creation — only `rotate()` existed, and it reissues the secret under the
48
+ * *same* name/scope, never a changed one. Every field absent from the patch
49
+ * is left exactly as saved; `rateLimitPerMinute: null` clears an explicit
50
+ * quota back to `DEFAULT_RATE_LIMIT_PER_MINUTE`, matching how omitting it at
51
+ * `create()` time already behaves.
52
+ */
53
+ export interface UpdateApiKeyInput {
54
+ readonly name?: string;
55
+ readonly scope?: readonly string[];
56
+ readonly rateLimitPerMinute?: number | null;
57
+ }
5
58
  export interface ApiKeyStore {
6
59
  /** Mints a key and returns the raw secret once. It is never stored or returned again. */
7
60
  create(input: CreateApiKeyInput): Promise<IssuedApiKey>;
8
- /** Never the raw key or its hash a prefix is all a list ever shows. */
9
- list(): Promise<readonly ApiKey[]>;
61
+ /** Tri-state patch see `UpdateApiKeyInput`. Never touches the key's secret, prefix, or lifecycle fields. */
62
+ update(id: string, patch: UpdateApiKeyInput): Promise<ApiKey>;
63
+ /** Never the raw key or its hash — a prefix is all a list ever shows. Newest first; `limit`/`offset` page it (fiche 67 task 5), both absent means "every key". */
64
+ list(options?: ListApiKeysOptions): Promise<readonly ApiKey[]>;
65
+ getById(id: string): Promise<ApiKey | null>;
10
66
  revoke(id: string): Promise<void>;
67
+ /**
68
+ * Fiche 62 task 2: a real, permanent `DELETE` — the row and its usage
69
+ * history are gone, never just hidden. Refuses anything but a key that is
70
+ * both revoked and has been for at least `MIN_PURGE_AFTER_REVOKED_DAYS`:
71
+ * an active key (even one technically past its `expiresAt`) is never
72
+ * purgeable, and neither is a key revoked too recently — see the piège
73
+ * connu in fiche 62 §5.
74
+ */
75
+ purge(id: string): Promise<void>;
76
+ /**
77
+ * Fiche 62 task 3, decision (b): recovers from a key revoked by mistake
78
+ * *without* lifting `revoked_at` — the revoked row stays revoked forever,
79
+ * exactly as `statusOf` in the admin already assumes. Instead, within
80
+ * `RECOVERY_WINDOW_MS` of the revocation, this mints a fresh replacement
81
+ * carrying the same name, scope and quota, the same way `rotate` does for
82
+ * a live key. Refuses a key that was never revoked (nothing to recover
83
+ * from — rotate it instead) and a key revoked longer ago than the window.
84
+ */
85
+ recover(id: string): Promise<IssuedApiKey>;
11
86
  /**
12
87
  * Resolves a raw bearer token to the key it names, or `null` if it does not
13
88
  * exist, is revoked, or has expired. Touches `lastUsedAt` on success, the
14
- * same sliding-window courtesy a session gets.
89
+ * same sliding-window courtesy a session gets, and counts the call toward
90
+ * this key's aggregated daily usage (fiche 20 task 4).
15
91
  */
16
92
  verify(rawKey: string): Promise<ApiKey | null>;
93
+ /**
94
+ * "Faire tourner cette clé" (fiche 20 task 2): mints a replacement carrying
95
+ * the same name, scope and quota, and leaves the original valid for
96
+ * `graceMs` longer rather than revoking it outright — a rotation with no
97
+ * window where nothing authenticates.
98
+ *
99
+ * Refuses a key that is already revoked or expired: there is nothing left
100
+ * to hand a grace period to, and calling that "rotation" would hide that
101
+ * the integration already stopped working.
102
+ */
103
+ rotate(id: string, options: RotateApiKeyOptions): Promise<ApiKeyRotationResult>;
104
+ /** Aggregated call counts over the last 7 and 30 days — never a line per call. */
105
+ usage(id: string): Promise<ApiKeyUsage>;
17
106
  }
18
107
  export declare function createApiKeyStore(db: DatabaseHandle, now?: () => number): ApiKeyStore;
19
108
  //# sourceMappingURL=api-keys.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"api-keys.d.ts","sourceRoot":"","sources":["../src/api-keys.ts"],"names":[],"mappings":"AACA,OAAO,EAAgB,KAAK,cAAc,EAA0B,MAAM,eAAe,CAAA;AAEzF,OAAO,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAkCzE,iGAAiG;AACjG,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEtD;AAsCD,MAAM,WAAW,WAAW;IAC1B,yFAAyF;IACzF,MAAM,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;IACvD,yEAAyE;IACzE,IAAI,IAAI,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAAA;IAClC,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACjC;;;;OAIG;IACH,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;CAC/C;AAED,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,cAAc,EAAE,GAAG,GAAE,MAAM,MAAiB,GAAG,WAAW,CA8D/F"}
1
+ {"version":3,"file":"api-keys.d.ts","sourceRoot":"","sources":["../src/api-keys.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,cAAc,EAKpB,MAAM,eAAe,CAAA;AAEtB,OAAO,KAAK,EACV,MAAM,EACN,oBAAoB,EACpB,WAAW,EACX,iBAAiB,EACjB,YAAY,EACb,MAAM,YAAY,CAAA;AA0BnB;;;;;;GAMG;AACH,eAAO,MAAM,6BAA6B,MAAM,CAAA;AAEhD;;;;;;;GAOG;AACH,eAAO,MAAM,4BAA4B,KAAK,CAAA;AAE9C;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,QAAsB,CAAA;AAWrD,iGAAiG;AACjG,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEtD;AAuDD,MAAM,WAAW,mBAAmB;IAClC,qEAAqE;IACrE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACzB;AAED;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CACzB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAClC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC5C;AAED,MAAM,WAAW,WAAW;IAC1B,yFAAyF;IACzF,MAAM,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;IACvD,8GAA8G;IAC9G,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAC7D,kKAAkK;IAClK,IAAI,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAAA;IAC9D,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IAC3C,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACjC;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAChC;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;IAC1C;;;;;OAKG;IACH,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IAC9C;;;;;;;;;OASG;IACH,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAA;IAC/E,kFAAkF;IAClF,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;CACxC;AAED,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,cAAc,EAAE,GAAG,GAAE,MAAM,MAAiB,GAAG,WAAW,CA6S/F"}
package/dist/api-keys.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { createHash, randomBytes } from 'node:crypto';
2
- import { CogentaError, identifier, newId, sql } from '@cogenta/core';
2
+ import { CogentaError, identifier, newId, sql, limit as sqlLimit, } from '@cogenta/core';
3
3
  import { TABLES } from './tables.js';
4
4
  /**
5
- * Machine-to-machine bearer credentials (L13 task 8).
5
+ * Machine-to-machine bearer credentials (L13 task 8; lifecycle, quota and
6
+ * usage added by fiche 20).
6
7
  *
7
8
  * `resolveActor` in `@cogenta/api` already recognises one bearer-token shape:
8
9
  * a session, minted at sign-in for a human. This store mints the other
@@ -22,6 +23,32 @@ const KEY_BYTES = 32;
22
23
  const KEY_PREFIX = 'cogenta_sk_';
23
24
  /** How much of the raw key stays visible forever, so a list of keys is recognisable. */
24
25
  const VISIBLE_PREFIX_LENGTH = 12;
26
+ /**
27
+ * A generous but real per-key request quota (fiche 20, "décisions à
28
+ * prendre"): high enough that no legitimate integration ever notices it,
29
+ * low enough that a leaked key cannot read the whole site as fast as the
30
+ * network allows. Applied whenever a key is minted — by `create` or by
31
+ * `rotate` — without an explicit quota of its own.
32
+ */
33
+ export const DEFAULT_RATE_LIMIT_PER_MINUTE = 600;
34
+ /**
35
+ * Fiche 62 task 2: how long a revoked key has to sit before it can be
36
+ * purged — the same reasoning and the same default as content's own
37
+ * `DEFAULT_TRASH_RETAIN_DAYS` (`@cogenta/schema`), applied here because a
38
+ * revoked key is not stored anywhere else: purging too early is the one
39
+ * mistake `purge()` must never make, since it is a real `DELETE`, not a
40
+ * second layer of soft state on top of `revoked_at`.
41
+ */
42
+ export const MIN_PURGE_AFTER_REVOKED_DAYS = 30;
43
+ /**
44
+ * Fiche 62 task 3 (decision "(b)", recommended by the fiche): a key revoked
45
+ * by mistake is not reactivated — `revoked_at` never goes back to `null` —
46
+ * but a short window after the mistake, `recover()` mints a replacement the
47
+ * same way `rotate()` does for an active key. Past this window the only way
48
+ * back is minting a brand new key from scratch, on purpose: the longer a key
49
+ * has been dead, the less likely "revoked by mistake" is the true story.
50
+ */
51
+ export const RECOVERY_WINDOW_MS = 24 * 60 * 60 * 1000;
25
52
  function issueKey() {
26
53
  return `${KEY_PREFIX}${randomBytes(KEY_BYTES).toString('base64url')}`;
27
54
  }
@@ -33,6 +60,10 @@ function hashKey(key) {
33
60
  export function looksLikeApiKey(token) {
34
61
  return token.startsWith(KEY_PREFIX) && token.length > KEY_PREFIX.length;
35
62
  }
63
+ /** `YYYY-MM-DD`, UTC — the same day boundary everywhere, regardless of server timezone. */
64
+ function dayKey(ms) {
65
+ return new Date(ms).toISOString().slice(0, 10);
66
+ }
36
67
  function fromRow(row) {
37
68
  return {
38
69
  id: row.id,
@@ -44,6 +75,8 @@ function fromRow(row) {
44
75
  expiresAt: row.expires_at ?? undefined,
45
76
  revokedAt: row.revoked_at ?? undefined,
46
77
  lastUsedAt: row.last_used_at ?? undefined,
78
+ rateLimitPerMinute: row.rate_limit_per_minute ?? DEFAULT_RATE_LIMIT_PER_MINUTE,
79
+ supersededBy: row.superseded_by ?? undefined,
47
80
  };
48
81
  }
49
82
  function assertScope(scope) {
@@ -55,42 +88,172 @@ function assertScope(scope) {
55
88
  });
56
89
  }
57
90
  }
91
+ function keyNotFound() {
92
+ return new CogentaError({
93
+ code: 'API_KEY_NOT_FOUND',
94
+ message: 'No API key with that id.',
95
+ hint: 'It may already have been revoked and removed from the list, or the id may be mistyped.',
96
+ });
97
+ }
58
98
  export function createApiKeyStore(db, now = Date.now) {
59
99
  const table = identifier(TABLES.apiKeys, db.dialect);
60
- return {
61
- create: async (input) => {
62
- assertScope(input.scope);
63
- const key = issueKey();
64
- const id = newId(now);
65
- const created = new Date(now()).toISOString();
66
- const prefix = key.slice(0, VISIBLE_PREFIX_LENGTH);
67
- await db.query(sql `
100
+ const usageTable = identifier(TABLES.apiKeyUsage, db.dialect);
101
+ async function create(input) {
102
+ assertScope(input.scope);
103
+ const key = issueKey();
104
+ const id = newId(now);
105
+ const created = new Date(now()).toISOString();
106
+ const prefix = key.slice(0, VISIBLE_PREFIX_LENGTH);
107
+ const rateLimitPerMinute = input.rateLimitPerMinute ?? DEFAULT_RATE_LIMIT_PER_MINUTE;
108
+ await db.query(sql `
68
109
  insert into ${table}
69
- (id, name, key_hash, key_prefix, scope, created_by, created_at, expires_at, revoked_at, last_used_at)
110
+ (id, name, key_hash, key_prefix, scope, created_by, created_at, expires_at, revoked_at,
111
+ last_used_at, rate_limit_per_minute, superseded_by)
70
112
  values
71
113
  (${id}, ${input.name}, ${hashKey(key)}, ${prefix}, ${JSON.stringify(input.scope)},
72
- ${input.createdBy}, ${created}, ${input.expiresAt ?? null}, ${null}, ${null})`);
73
- return {
74
- id,
75
- name: input.name,
76
- prefix,
77
- scope: input.scope,
78
- createdBy: input.createdBy,
79
- createdAt: created,
80
- expiresAt: input.expiresAt,
81
- revokedAt: undefined,
82
- lastUsedAt: undefined,
83
- key,
84
- };
85
- },
86
- list: async () => {
87
- const result = await db.query(sql `select * from ${table} order by created_at desc`);
114
+ ${input.createdBy}, ${created}, ${input.expiresAt ?? null}, ${null},
115
+ ${null}, ${rateLimitPerMinute}, ${null})`);
116
+ return {
117
+ id,
118
+ name: input.name,
119
+ prefix,
120
+ scope: input.scope,
121
+ createdBy: input.createdBy,
122
+ createdAt: created,
123
+ expiresAt: input.expiresAt,
124
+ revokedAt: undefined,
125
+ lastUsedAt: undefined,
126
+ rateLimitPerMinute,
127
+ supersededBy: undefined,
128
+ key,
129
+ };
130
+ }
131
+ async function recordUsage(id) {
132
+ const day = dayKey(now());
133
+ const updated = await db.query(sql `update ${usageTable} set count = count + 1 where key_id = ${id} and day = ${day}`);
134
+ if (updated.rowsAffected > 0)
135
+ return;
136
+ // First call of the day for this key: insert the row. A concurrent first
137
+ // call could lose this race — the unique index on (key_id, day) then
138
+ // refuses the second insert, and the fallback update picks up the count
139
+ // that insert would have started at 1.
140
+ await db
141
+ .query(sql `insert into ${usageTable} (id, key_id, day, count) values (${newId(now)}, ${id}, ${day}, ${1})`)
142
+ .catch(() => db.query(sql `update ${usageTable} set count = count + 1 where key_id = ${id} and day = ${day}`));
143
+ }
144
+ return {
145
+ create,
146
+ list: async (options) => {
147
+ let statement = sql `select * from ${table} order by created_at desc, id desc`;
148
+ if (options?.limit !== undefined) {
149
+ statement = sql `${statement} limit ${sqlLimit(options.limit)} offset ${sqlLimit(options.offset ?? 0)}`;
150
+ }
151
+ const result = await db.query(statement);
88
152
  return result.rows.map(fromRow);
89
153
  },
154
+ getById: async (id) => {
155
+ const result = await db.query(sql `select * from ${table} where id = ${id}`);
156
+ const row = result.rows[0];
157
+ return row === undefined ? null : fromRow(row);
158
+ },
159
+ update: async (id, patch) => {
160
+ const result = await db.query(sql `select * from ${table} where id = ${id}`);
161
+ const row = result.rows[0];
162
+ if (row === undefined)
163
+ throw keyNotFound();
164
+ if (patch.scope !== undefined)
165
+ assertScope(patch.scope);
166
+ const name = patch.name ?? row.name;
167
+ const scopeJson = patch.scope === undefined ? row.scope : JSON.stringify(patch.scope);
168
+ const rateLimitPerMinute = patch.rateLimitPerMinute === undefined
169
+ ? row.rate_limit_per_minute
170
+ : patch.rateLimitPerMinute;
171
+ await db.query(sql `
172
+ update ${table}
173
+ set name = ${name}, scope = ${scopeJson}, rate_limit_per_minute = ${rateLimitPerMinute}
174
+ where id = ${id}`);
175
+ return fromRow({
176
+ ...row,
177
+ name,
178
+ scope: scopeJson,
179
+ rate_limit_per_minute: rateLimitPerMinute,
180
+ });
181
+ },
90
182
  revoke: async (id) => {
91
183
  const revokedAt = new Date(now()).toISOString();
92
184
  await db.query(sql `update ${table} set revoked_at = ${revokedAt} where id = ${id} and revoked_at is null`);
93
185
  },
186
+ purge: async (id) => {
187
+ const result = await db.query(sql `select * from ${table} where id = ${id}`);
188
+ const row = result.rows[0];
189
+ if (row === undefined)
190
+ throw keyNotFound();
191
+ if (row.revoked_at === null) {
192
+ throw new CogentaError({
193
+ code: 'API_KEY_PURGE_INVALID',
194
+ message: 'Only a revoked key can be purged.',
195
+ hint: 'Revoke this key first if it should no longer exist at all.',
196
+ });
197
+ }
198
+ const revokedAgoMs = now() - new Date(row.revoked_at).getTime();
199
+ const minAgeMs = MIN_PURGE_AFTER_REVOKED_DAYS * 24 * 60 * 60 * 1000;
200
+ if (revokedAgoMs < minAgeMs) {
201
+ throw new CogentaError({
202
+ code: 'API_KEY_PURGE_INVALID',
203
+ message: `A revoked key can only be purged after ${MIN_PURGE_AFTER_REVOKED_DAYS} days.`,
204
+ hint: 'Wait for the retention window to pass, or leave it revoked — a revoked key can never authenticate again.',
205
+ });
206
+ }
207
+ // Its usage history first: nothing references it by foreign key, but a
208
+ // dangling row for a key id that no longer exists would only ever be
209
+ // dead weight, never something `usage()` is asked about again.
210
+ await db.query(sql `delete from ${usageTable} where key_id = ${id}`);
211
+ await db.query(sql `delete from ${table} where id = ${id}`);
212
+ },
213
+ recover: async (id) => {
214
+ const result = await db.query(sql `select * from ${table} where id = ${id}`);
215
+ const row = result.rows[0];
216
+ if (row === undefined)
217
+ throw keyNotFound();
218
+ if (row.revoked_at === null) {
219
+ throw new CogentaError({
220
+ code: 'API_KEY_RECOVERY_INVALID',
221
+ message: 'Only a revoked key can be recovered — an active key should be rotated instead.',
222
+ hint: 'Use rotate() for a key that is still active.',
223
+ });
224
+ }
225
+ const revokedAgoMs = now() - new Date(row.revoked_at).getTime();
226
+ if (revokedAgoMs > RECOVERY_WINDOW_MS) {
227
+ throw new CogentaError({
228
+ code: 'API_KEY_RECOVERY_INVALID',
229
+ message: `This key was revoked more than ${RECOVERY_WINDOW_MS / (60 * 60 * 1000)} hours ago — recovery is only for a very recent, mistaken revocation.`,
230
+ hint: 'Create a brand new key instead. The revoked key stays revoked either way.',
231
+ });
232
+ }
233
+ const scope = JSON.parse(row.scope);
234
+ const rateLimitPerMinute = row.rate_limit_per_minute ?? DEFAULT_RATE_LIMIT_PER_MINUTE;
235
+ const currentTime = now();
236
+ // Same duration-preserving policy as rotate(): a replacement for a key
237
+ // that never expired keeps not expiring, and one that had a fixed
238
+ // lifetime gets that same duration, restarted from now.
239
+ const expiresAt = row.expires_at === null
240
+ ? undefined
241
+ : new Date(currentTime +
242
+ Math.max(new Date(row.expires_at).getTime() - new Date(row.created_at).getTime(), 0)).toISOString();
243
+ const issued = await create({
244
+ name: row.name,
245
+ scope,
246
+ createdBy: row.created_by,
247
+ rateLimitPerMinute,
248
+ ...(expiresAt === undefined ? {} : { expiresAt }),
249
+ });
250
+ // `revoked_at` is deliberately never touched: the row this recovers
251
+ // from stays revoked forever, exactly as decision (b) in fiche 62 §3
252
+ // requires. `superseded_by` only records what replaced it, the same
253
+ // way rotation already links a live key to what came after it.
254
+ await db.query(sql `update ${table} set superseded_by = ${issued.id} where id = ${id}`);
255
+ return issued;
256
+ },
94
257
  verify: async (rawKey) => {
95
258
  if (!looksLikeApiKey(rawKey))
96
259
  return null;
@@ -105,8 +268,78 @@ export function createApiKeyStore(db, now = Date.now) {
105
268
  return null;
106
269
  const lastUsedAt = new Date(now()).toISOString();
107
270
  await db.query(sql `update ${table} set last_used_at = ${lastUsedAt} where id = ${row.id}`);
271
+ await recordUsage(row.id);
108
272
  return fromRow({ ...row, last_used_at: lastUsedAt });
109
273
  },
274
+ rotate: async (id, options) => {
275
+ const result = await db.query(sql `select * from ${table} where id = ${id}`);
276
+ const row = result.rows[0];
277
+ if (row === undefined)
278
+ throw keyNotFound();
279
+ if (row.revoked_at !== null) {
280
+ throw new CogentaError({
281
+ code: 'API_KEY_ROTATION_INVALID',
282
+ message: 'A revoked key cannot be rotated.',
283
+ hint: 'Create a new key instead — there is nothing left to hand a grace period to.',
284
+ });
285
+ }
286
+ const currentTime = now();
287
+ if (row.expires_at !== null && new Date(row.expires_at).getTime() <= currentTime) {
288
+ throw new CogentaError({
289
+ code: 'API_KEY_ROTATION_INVALID',
290
+ message: 'An expired key cannot be rotated.',
291
+ hint: 'Create a new key instead — there is nothing left to hand a grace period to.',
292
+ });
293
+ }
294
+ const scope = JSON.parse(row.scope);
295
+ const rateLimitPerMinute = row.rate_limit_per_minute ?? DEFAULT_RATE_LIMIT_PER_MINUTE;
296
+ // The new key's own expiry policy preserves the *duration* the old key
297
+ // was valid for, restarted from now — never its literal calendar date,
298
+ // which could already be imminent. A key that never expired keeps not
299
+ // expiring; nothing here invents a default the caller did not ask for.
300
+ const expiresAt = row.expires_at === null
301
+ ? undefined
302
+ : new Date(currentTime +
303
+ Math.max(new Date(row.expires_at).getTime() - new Date(row.created_at).getTime(), 0)).toISOString();
304
+ const issued = await create({
305
+ name: row.name,
306
+ scope,
307
+ createdBy: row.created_by,
308
+ rateLimitPerMinute,
309
+ ...(expiresAt === undefined ? {} : { expiresAt }),
310
+ });
311
+ // The old key's grace window: it keeps working until whichever comes
312
+ // first, its own existing expiry or the chosen grace period — rotating
313
+ // a key that was about to expire tomorrow must not grant it another
314
+ // week.
315
+ const graceExpiry = new Date(currentTime + options.graceMs).toISOString();
316
+ const previousExpiresAt = row.expires_at !== null && row.expires_at < graceExpiry ? row.expires_at : graceExpiry;
317
+ await db.query(sql `
318
+ update ${table} set expires_at = ${previousExpiresAt}, superseded_by = ${issued.id}
319
+ where id = ${id}`);
320
+ return {
321
+ issued,
322
+ previous: fromRow({
323
+ ...row,
324
+ expires_at: previousExpiresAt,
325
+ superseded_by: issued.id,
326
+ }),
327
+ };
328
+ },
329
+ usage: async (id) => {
330
+ const since30 = dayKey(now() - 29 * 24 * 60 * 60 * 1000);
331
+ const since7 = dayKey(now() - 6 * 24 * 60 * 60 * 1000);
332
+ const result = await db.query(sql `select day, count from ${usageTable} where key_id = ${id} and day >= ${since30}`);
333
+ let last7Days = 0;
334
+ let last30Days = 0;
335
+ for (const row of result.rows) {
336
+ const count = Number(row.count);
337
+ last30Days += count;
338
+ if (row.day >= since7)
339
+ last7Days += count;
340
+ }
341
+ return { last7Days, last30Days };
342
+ },
110
343
  };
111
344
  }
112
345
  //# sourceMappingURL=api-keys.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"api-keys.js","sourceRoot":"","sources":["../src/api-keys.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"api-keys.js","sourceRoot":"","sources":["../src/api-keys.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,61 @@
1
+ import { type DatabaseHandle } from '@cogenta/core';
2
+ import type { AuditChainPoint, AuditLog } from './audit.js';
3
+ /**
4
+ * Fiche 21 task 3: "un bouton qu'on ne presse jamais ne protège de rien" — a
5
+ * scheduled counterpart to the manual "verify now" button, with somewhere to
6
+ * remember what it last found so the admin screen can say "checked 40
7
+ * seconds ago, chain intact" without replaying the whole log to answer that.
8
+ *
9
+ * Two things this store is careful about:
10
+ * - **Bounded.** Most runs are `audit.verifyRange(checkpoint)`, not
11
+ * `audit.verify()` — a million-entry log cannot be replayed on every
12
+ * tick. A full replay still happens periodically (`fullCheckIntervalMs`,
13
+ * a week by default) as the "vérification complète plus rare en secours"
14
+ * the fiche asks for, because an incremental check can only prove the
15
+ * *suffix* since the last checkpoint is intact — it does not re-read
16
+ * entries before it, so tampering with already-checkpointed history is
17
+ * only ever caught by a full run.
18
+ * - **Sticky once broken.** A break is not quietly retried away: once
19
+ * `state` is `'broken'`, only an explicit `check({ full: true })` (the
20
+ * admin's own "verify now", or an operator who has since pruned the bad
21
+ * prefix) attempts to move past it. Every other tick just re-stamps
22
+ * `lastCheckedAt` so "still checking" is visible without pretending the
23
+ * problem went away on its own.
24
+ */
25
+ export type AuditIntegrityMode = 'incremental' | 'full';
26
+ export type AuditIntegrityState = 'never-run' | 'ok' | 'broken';
27
+ export interface AuditIntegrityStatus {
28
+ readonly state: AuditIntegrityState;
29
+ readonly checkpoint: AuditChainPoint | null;
30
+ readonly entriesChecked: number;
31
+ readonly lastCheckedAt: string | null;
32
+ readonly lastMode: AuditIntegrityMode | null;
33
+ readonly lastFullCheckedAt: string | null;
34
+ readonly brokenAt: string | null;
35
+ readonly brokenEntryId: string | null;
36
+ readonly brokenMessage: string | null;
37
+ }
38
+ export interface AuditIntegrityCheckResult {
39
+ readonly status: AuditIntegrityStatus;
40
+ /**
41
+ * `true` only on the run that first found the chain broken — the signal
42
+ * an alert-sending caller should use to actually send one, rather than
43
+ * re-alerting on every subsequent tick while the same break stands.
44
+ */
45
+ readonly newlyBroken: boolean;
46
+ }
47
+ export interface AuditIntegrityStore {
48
+ /** Reads the last outcome without running a new check — cheap, one row. */
49
+ status(): Promise<AuditIntegrityStatus>;
50
+ /** Runs a check now. `full: true` forces a full replay instead of the usual incremental one — also what re-attempts past a `'broken'` state. */
51
+ check(options?: {
52
+ readonly full?: boolean;
53
+ }): Promise<AuditIntegrityCheckResult>;
54
+ }
55
+ export interface AuditIntegrityOptions {
56
+ readonly now?: () => number;
57
+ /** How rarely a full replay runs on its own. Default: 7 days. */
58
+ readonly fullCheckIntervalMs?: number;
59
+ }
60
+ export declare function createAuditIntegrityStore(db: DatabaseHandle, audit: Pick<AuditLog, 'verifyRange'>, options?: AuditIntegrityOptions): AuditIntegrityStore;
61
+ //# sourceMappingURL=audit-integrity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit-integrity.d.ts","sourceRoot":"","sources":["../src/audit-integrity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,cAAc,EAAmC,MAAM,eAAe,CAAA;AACpF,OAAO,KAAK,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAG3D;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,MAAM,MAAM,kBAAkB,GAAG,aAAa,GAAG,MAAM,CAAA;AACvD,MAAM,MAAM,mBAAmB,GAAG,WAAW,GAAG,IAAI,GAAG,QAAQ,CAAA;AAE/D,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,KAAK,EAAE,mBAAmB,CAAA;IACnC,QAAQ,CAAC,UAAU,EAAE,eAAe,GAAG,IAAI,CAAA;IAC3C,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;IAC/B,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IACrC,QAAQ,CAAC,QAAQ,EAAE,kBAAkB,GAAG,IAAI,CAAA;IAC5C,QAAQ,CAAC,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAA;IACzC,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IACrC,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;CACtC;AAED,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAA;IACrC;;;;OAIG;IACH,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAA;CAC9B;AAED,MAAM,WAAW,mBAAmB;IAClC,2EAA2E;IAC3E,MAAM,IAAI,OAAO,CAAC,oBAAoB,CAAC,CAAA;IACvC,gJAAgJ;IAChJ,KAAK,CAAC,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,yBAAyB,CAAC,CAAA;CACjF;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAC3B,iEAAiE;IACjE,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAA;CACtC;AA4DD,wBAAgB,yBAAyB,CACvC,EAAE,EAAE,cAAc,EAClB,KAAK,EAAE,IAAI,CAAC,QAAQ,EAAE,aAAa,CAAC,EACpC,OAAO,GAAE,qBAA0B,GAClC,mBAAmB,CAiGrB"}