@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.
- package/dist/api-keys.d.ts +93 -4
- package/dist/api-keys.d.ts.map +1 -1
- package/dist/api-keys.js +260 -27
- package/dist/api-keys.js.map +1 -1
- package/dist/audit-integrity.d.ts +61 -0
- package/dist/audit-integrity.d.ts.map +1 -0
- package/dist/audit-integrity.js +130 -0
- package/dist/audit-integrity.js.map +1 -0
- package/dist/audit.d.ts +78 -2
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +168 -24
- package/dist/audit.js.map +1 -1
- package/dist/credentials.d.ts +27 -0
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js +61 -0
- package/dist/credentials.js.map +1 -1
- package/dist/index.d.ts +11 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/login.d.ts +63 -6
- package/dist/login.d.ts.map +1 -1
- package/dist/login.js +90 -10
- package/dist/login.js.map +1 -1
- package/dist/mfa.d.ts +1 -1
- package/dist/mfa.d.ts.map +1 -1
- package/dist/mfa.js +3 -1
- package/dist/mfa.js.map +1 -1
- package/dist/recovery-codes.d.ts +25 -0
- package/dist/recovery-codes.d.ts.map +1 -0
- package/dist/recovery-codes.js +56 -0
- package/dist/recovery-codes.js.map +1 -0
- package/dist/resets.d.ts +15 -1
- package/dist/resets.d.ts.map +1 -1
- package/dist/resets.js +10 -0
- package/dist/resets.js.map +1 -1
- package/dist/sessions.d.ts +24 -0
- package/dist/sessions.d.ts.map +1 -1
- package/dist/sessions.js +25 -4
- package/dist/sessions.js.map +1 -1
- package/dist/store.d.ts +3 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +4 -1
- package/dist/store.js.map +1 -1
- package/dist/tables.d.ts +17 -0
- package/dist/tables.d.ts.map +1 -1
- package/dist/tables.js +129 -1
- package/dist/tables.js.map +1 -1
- package/dist/types.d.ts +89 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +10 -1
- package/dist/types.js.map +1 -1
- package/dist/user-agent.d.ts +18 -0
- package/dist/user-agent.d.ts.map +1 -0
- package/dist/user-agent.js +39 -0
- package/dist/user-agent.js.map +1 -0
- package/dist/users.d.ts +24 -1
- package/dist/users.d.ts.map +1 -1
- package/dist/users.js +72 -3
- package/dist/users.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { randomInt } from 'node:crypto';
|
|
2
|
+
import { hashPassword, verifyPassword } from './password.js';
|
|
3
|
+
/**
|
|
4
|
+
* TOTP recovery codes (fiche 18 task 1) — the way back in for an account that
|
|
5
|
+
* enrolled a second factor and then lost the device that produces it.
|
|
6
|
+
*
|
|
7
|
+
* Without this, `docs/lots/17-utilisateurs.md`'s deliberate decision not to let
|
|
8
|
+
* an admin reset somebody else's password, combined with a lost authenticator,
|
|
9
|
+
* is a permanent lockout. A recovery code is, in every way that matters, a
|
|
10
|
+
* spare password: ten of them are generated at once, shown to the person
|
|
11
|
+
* exactly once, and stored **hashed with the same scrypt as a real password**
|
|
12
|
+
* (`password.ts`) rather than a lighter or reversible encoding — a leaked
|
|
13
|
+
* credentials table must hand out nothing live here either.
|
|
14
|
+
*/
|
|
15
|
+
export const RECOVERY_CODE_COUNT = 10;
|
|
16
|
+
// No 0/O or 1/I: both pairs are read off a screen and typed back by hand, and
|
|
17
|
+
// this alphabet is chosen so no code is ambiguous doing that.
|
|
18
|
+
const CODE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
|
19
|
+
const GROUP_LENGTH = 5;
|
|
20
|
+
function randomGroup() {
|
|
21
|
+
let group = '';
|
|
22
|
+
for (let i = 0; i < GROUP_LENGTH; i += 1) {
|
|
23
|
+
group += CODE_ALPHABET[randomInt(CODE_ALPHABET.length)];
|
|
24
|
+
}
|
|
25
|
+
return group;
|
|
26
|
+
}
|
|
27
|
+
/** A fresh code, shaped `XXXXX-XXXXX` — 10 characters from a 32-letter alphabet, ~50 bits of entropy. */
|
|
28
|
+
function randomCode() {
|
|
29
|
+
return `${randomGroup()}-${randomGroup()}`;
|
|
30
|
+
}
|
|
31
|
+
/** `count` fresh, distinct plaintext codes. The caller shows them to the person exactly once and never logs them. */
|
|
32
|
+
export function generateRecoveryCodes(count = RECOVERY_CODE_COUNT) {
|
|
33
|
+
const codes = new Set();
|
|
34
|
+
while (codes.size < count)
|
|
35
|
+
codes.add(randomCode());
|
|
36
|
+
return [...codes];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* What a person actually types back rarely matches the display exactly —
|
|
40
|
+
* lowercase, a missing dash, stray whitespace copied along with it. None of
|
|
41
|
+
* that is part of the secret, so it is normalised away before hashing and
|
|
42
|
+
* before verifying, on both sides, the same way.
|
|
43
|
+
*/
|
|
44
|
+
export function normaliseRecoveryCode(input) {
|
|
45
|
+
return input
|
|
46
|
+
.trim()
|
|
47
|
+
.toUpperCase()
|
|
48
|
+
.replace(/[^A-Z0-9]/gu, '');
|
|
49
|
+
}
|
|
50
|
+
export function hashRecoveryCode(code) {
|
|
51
|
+
return hashPassword(normaliseRecoveryCode(code));
|
|
52
|
+
}
|
|
53
|
+
export function verifyRecoveryCode(code, hash) {
|
|
54
|
+
return verifyPassword(normaliseRecoveryCode(code), hash);
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=recovery-codes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recovery-codes.js","sourceRoot":"","sources":["../src/recovery-codes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACvC,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAE5D;;;;;;;;;;;GAWG;AAEH,MAAM,CAAC,MAAM,mBAAmB,GAAG,EAAE,CAAA;AAErC,8EAA8E;AAC9E,8DAA8D;AAC9D,MAAM,aAAa,GAAG,kCAAkC,CAAA;AACxD,MAAM,YAAY,GAAG,CAAC,CAAA;AAEtB,SAAS,WAAW;IAClB,IAAI,KAAK,GAAG,EAAE,CAAA;IACd,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,YAAY,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QACzC,KAAK,IAAI,aAAa,CAAC,SAAS,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAA;IACzD,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED,yGAAyG;AACzG,SAAS,UAAU;IACjB,OAAO,GAAG,WAAW,EAAE,IAAI,WAAW,EAAE,EAAE,CAAA;AAC5C,CAAC;AAED,qHAAqH;AACrH,MAAM,UAAU,qBAAqB,CAAC,KAAK,GAAW,mBAAmB;IACvE,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAA;IAC/B,OAAO,KAAK,CAAC,IAAI,GAAG,KAAK;QAAE,KAAK,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC,CAAA;IAClD,OAAO,CAAC,GAAG,KAAK,CAAC,CAAA;AACnB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAAa;IACjD,OAAO,KAAK;SACT,IAAI,EAAE;SACN,WAAW,EAAE;SACb,OAAO,CAAC,aAAa,EAAE,EAAE,CAAC,CAAA;AAC/B,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,OAAO,YAAY,CAAC,qBAAqB,CAAC,IAAI,CAAC,CAAC,CAAA;AAClD,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,IAAY,EAAE,IAAY;IAC3D,OAAO,cAAc,CAAC,qBAAqB,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,CAAA;AAC1D,CAAC"}
|
package/dist/resets.d.ts
CHANGED
|
@@ -29,13 +29,25 @@ export type PasswordResetOutcome = {
|
|
|
29
29
|
readonly userId: string;
|
|
30
30
|
readonly resetId: string;
|
|
31
31
|
};
|
|
32
|
+
/**
|
|
33
|
+
* What `pending` reports for a still-usable token — fiche 17 task 1's "état «
|
|
34
|
+
* invitation envoyée le … »", read off the very row `issue` already wrote
|
|
35
|
+
* rather than a second, parallel piece of state to keep in sync with it.
|
|
36
|
+
*/
|
|
37
|
+
export interface PendingReset {
|
|
38
|
+
readonly issuedAt: string;
|
|
39
|
+
readonly expiresAt: string;
|
|
40
|
+
}
|
|
32
41
|
export interface PasswordResetStore {
|
|
33
42
|
/**
|
|
34
43
|
* Issues a token for a user, invalidating any still outstanding for them.
|
|
35
44
|
*
|
|
36
45
|
* Only one live reset per person: asking again because the first mail never
|
|
37
46
|
* arrived must not leave two working links behind, and the newest request
|
|
38
|
-
* is always the one the person is actually looking at.
|
|
47
|
+
* is always the one the person is actually looking at. This is also how a
|
|
48
|
+
* resend works for an invitation (fiche 17 task 1): calling `issue` again
|
|
49
|
+
* both replaces the link and answers "which token is live" with no
|
|
50
|
+
* ambiguity.
|
|
39
51
|
*/
|
|
40
52
|
issue(userId: string, options?: {
|
|
41
53
|
readonly ttlMs?: number;
|
|
@@ -48,6 +60,8 @@ export interface PasswordResetStore {
|
|
|
48
60
|
redeem(token: string): Promise<PasswordResetOutcome>;
|
|
49
61
|
/** Invalidates every outstanding reset for a user, used or not. */
|
|
50
62
|
revokeAllFor(userId: string): Promise<void>;
|
|
63
|
+
/** The still-usable token for this user, if any — never the token itself, which is only ever shown once, at `issue`. */
|
|
64
|
+
pending(userId: string): Promise<PendingReset | null>;
|
|
51
65
|
}
|
|
52
66
|
export declare function createPasswordResetStore(db: DatabaseHandle, now?: () => number): PasswordResetStore;
|
|
53
67
|
//# sourceMappingURL=resets.d.ts.map
|
package/dist/resets.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resets.d.ts","sourceRoot":"","sources":["../src/resets.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,cAAc,EAA0B,MAAM,eAAe,CAAA;AAoC3E;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,QAAiB,CAAA;AAEnD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,0EAA0E;IAC1E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED;;;;GAIG;AACH,MAAM,MAAM,oBAAoB,GAC5B;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GAC5B;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GAC5B;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AAEjF,MAAM,WAAW,kBAAkB;IACjC
|
|
1
|
+
{"version":3,"file":"resets.d.ts","sourceRoot":"","sources":["../src/resets.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,cAAc,EAA0B,MAAM,eAAe,CAAA;AAoC3E;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,QAAiB,CAAA;AAEnD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,0EAA0E;IAC1E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED;;;;GAIG;AACH,MAAM,MAAM,oBAAoB,GAC5B;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GAC5B;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GAC5B;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AAEjF;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED,MAAM,WAAW,kBAAkB;IACjC;;;;;;;;;OASG;IACH,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAA;IAC1F;;;;OAIG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAA;IACpD,mEAAmE;IACnE,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC3C,wHAAwH;IACxH,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAA;CACtD;AAkBD,wBAAgB,wBAAwB,CACtC,EAAE,EAAE,cAAc,EAClB,GAAG,GAAE,MAAM,MAAiB,GAC3B,kBAAkB,CA8DpB"}
|
package/dist/resets.js
CHANGED
|
@@ -84,6 +84,16 @@ export function createPasswordResetStore(db, now = Date.now) {
|
|
|
84
84
|
revokeAllFor: async (userId) => {
|
|
85
85
|
await db.query(sql `delete from ${table} where user_id = ${userId}`);
|
|
86
86
|
},
|
|
87
|
+
pending: async (userId) => {
|
|
88
|
+
const result = await db.query(sql `select * from ${table} where user_id = ${userId} and used_at is null
|
|
89
|
+
order by created_at desc limit ${1}`);
|
|
90
|
+
const row = result.rows[0];
|
|
91
|
+
if (row === undefined)
|
|
92
|
+
return null;
|
|
93
|
+
if (new Date(row.expires_at).getTime() <= now())
|
|
94
|
+
return null;
|
|
95
|
+
return { issuedAt: row.created_at, expiresAt: row.expires_at };
|
|
96
|
+
},
|
|
87
97
|
};
|
|
88
98
|
}
|
|
89
99
|
//# sourceMappingURL=resets.js.map
|
package/dist/resets.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resets.js","sourceRoot":"","sources":["../src/resets.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACrD,OAAO,EAAuB,UAAU,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,eAAe,CAAA;AAC3E,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEpC;;;;;;;;;;;;;;GAcG;AAEH,MAAM,WAAW,GAAG,EAAE,CAAA;AAEtB;;;;;;;;;;GAUG;AACH,SAAS,WAAW,CAAC,KAAa;IAChC,OAAO,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;AAC9B,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA;AA+
|
|
1
|
+
{"version":3,"file":"resets.js","sourceRoot":"","sources":["../src/resets.ts"],"names":[],"mappings":"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"}
|
package/dist/sessions.d.ts
CHANGED
|
@@ -4,6 +4,12 @@ export interface SessionStore {
|
|
|
4
4
|
create(userId: string, options?: {
|
|
5
5
|
label?: string;
|
|
6
6
|
ttlMs?: number;
|
|
7
|
+
/**
|
|
8
|
+
* Raw `User-Agent` header, if any. Distilled to a browser family and a
|
|
9
|
+
* device type before it ever reaches SQL (`user-agent.ts`) — the header
|
|
10
|
+
* itself is never stored (fiche 18 task 2).
|
|
11
|
+
*/
|
|
12
|
+
userAgent?: string;
|
|
7
13
|
}): Promise<IssuedSession>;
|
|
8
14
|
/**
|
|
9
15
|
* Resolves a bearer token to its session, or `null` if it is missing,
|
|
@@ -15,6 +21,24 @@ export interface SessionStore {
|
|
|
15
21
|
list(userId: string): Promise<readonly Session[]>;
|
|
16
22
|
revoke(sessionId: string): Promise<void>;
|
|
17
23
|
revokeAll(userId: string): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* The last time each account was seen, across every session it has ever
|
|
26
|
+
* held — revoked and expired ones included, not just the live ones `list`
|
|
27
|
+
* returns (fiche 17 task 4's "last sign-in" column and dormant-account
|
|
28
|
+
* signal). `last_seen_at` is only ever advanced by a *successful* `resolve`
|
|
29
|
+
* (never by `create` alone), so this answers "when did this account last do
|
|
30
|
+
* something", not "when was its most recent token minted". One query for
|
|
31
|
+
* every account rather than one per account — the users admin screen loads
|
|
32
|
+
* every account already, at the scale fiche 17 targets (about a hundred).
|
|
33
|
+
*/
|
|
34
|
+
lastSeenByUser(): Promise<ReadonlyMap<string, string>>;
|
|
35
|
+
/**
|
|
36
|
+
* Revokes every live session this user has **except** `keepSessionId` —
|
|
37
|
+
* "sign out everywhere else" (fiche 18 task 2). Returns how many sessions
|
|
38
|
+
* were actually revoked, so the caller can report a real number rather than
|
|
39
|
+
* a bare "done".
|
|
40
|
+
*/
|
|
41
|
+
revokeAllExcept(userId: string, keepSessionId: string): Promise<number>;
|
|
18
42
|
}
|
|
19
43
|
export declare function createSessionStore(db: DatabaseHandle, now?: () => number): SessionStore;
|
|
20
44
|
//# sourceMappingURL=sessions.d.ts.map
|
package/dist/sessions.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../src/sessions.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,cAAc,EAA0B,MAAM,eAAe,CAAA;AAE3E,OAAO,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../src/sessions.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,cAAc,EAA0B,MAAM,eAAe,CAAA;AAE3E,OAAO,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAiDxD,MAAM,WAAW,YAAY;IAC3B,MAAM,CACJ,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE;QACR,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,KAAK,CAAC,EAAE,MAAM,CAAA;QACd;;;;WAIG;QACH,SAAS,CAAC,EAAE,MAAM,CAAA;KACnB,GACA,OAAO,CAAC,aAAa,CAAC,CAAA;IACzB;;;;;OAKG;IACH,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAA;IAC/C,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,OAAO,EAAE,CAAC,CAAA;IACjD,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACxC,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACxC;;;;;;;;;OASG;IACH,cAAc,IAAI,OAAO,CAAC,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACtD;;;;;OAKG;IACH,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;CACxE;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,cAAc,EAAE,GAAG,GAAE,MAAM,MAAiB,GAAG,YAAY,CAiFjG"}
|
package/dist/sessions.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash, randomBytes } from 'node:crypto';
|
|
2
2
|
import { identifier, newId, sql } from '@cogenta/core';
|
|
3
3
|
import { TABLES } from './tables.js';
|
|
4
|
+
import { parseUserAgent } from './user-agent.js';
|
|
4
5
|
const TOKEN_BYTES = 32;
|
|
5
6
|
const DEFAULT_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days, sliding on use.
|
|
6
7
|
/**
|
|
@@ -27,6 +28,8 @@ function fromRow(row) {
|
|
|
27
28
|
expiresAt: row.expires_at,
|
|
28
29
|
lastSeenAt: row.last_seen_at,
|
|
29
30
|
label: row.label ?? undefined,
|
|
31
|
+
browser: row.browser ?? 'unknown',
|
|
32
|
+
device: row.device ?? 'unknown',
|
|
30
33
|
};
|
|
31
34
|
}
|
|
32
35
|
export function createSessionStore(db, now = Date.now) {
|
|
@@ -35,11 +38,18 @@ export function createSessionStore(db, now = Date.now) {
|
|
|
35
38
|
create: async (userId, options) => {
|
|
36
39
|
const token = issueToken();
|
|
37
40
|
const id = newId(now);
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
// One instant, not two separate `now()` calls: `created`/`expires` are
|
|
42
|
+
// both derived from it, so `expiresAt - createdAt` is exactly `ttlMs`
|
|
43
|
+
// rather than off by however many milliseconds elapsed between two
|
|
44
|
+
// clock reads — the gap that made `remember me`'s duration assertion
|
|
45
|
+
// (fiche 18 task 5) flaky under real timing.
|
|
46
|
+
const nowMs = now();
|
|
47
|
+
const created = new Date(nowMs).toISOString();
|
|
48
|
+
const expires = new Date(nowMs + (options?.ttlMs ?? DEFAULT_TTL_MS)).toISOString();
|
|
49
|
+
const { browser, device } = parseUserAgent(options?.userAgent);
|
|
40
50
|
await db.query(sql `
|
|
41
|
-
insert into ${table} (id, user_id, token_hash, label, created_at, expires_at, last_seen_at, revoked)
|
|
42
|
-
values (${id}, ${userId}, ${hashToken(token)}, ${options?.label ?? null}, ${created}, ${expires}, ${created}, ${false})`);
|
|
51
|
+
insert into ${table} (id, user_id, token_hash, label, created_at, expires_at, last_seen_at, revoked, browser, device)
|
|
52
|
+
values (${id}, ${userId}, ${hashToken(token)}, ${options?.label ?? null}, ${created}, ${expires}, ${created}, ${false}, ${browser}, ${device})`);
|
|
43
53
|
return {
|
|
44
54
|
id,
|
|
45
55
|
userId,
|
|
@@ -48,6 +58,8 @@ export function createSessionStore(db, now = Date.now) {
|
|
|
48
58
|
expiresAt: expires,
|
|
49
59
|
lastSeenAt: created,
|
|
50
60
|
label: options?.label,
|
|
61
|
+
browser,
|
|
62
|
+
device,
|
|
51
63
|
};
|
|
52
64
|
},
|
|
53
65
|
resolve: async (token) => {
|
|
@@ -77,6 +89,15 @@ export function createSessionStore(db, now = Date.now) {
|
|
|
77
89
|
revokeAll: async (userId) => {
|
|
78
90
|
await db.query(sql `update ${table} set revoked = ${true} where user_id = ${userId}`);
|
|
79
91
|
},
|
|
92
|
+
lastSeenByUser: async () => {
|
|
93
|
+
const result = await db.query(sql `
|
|
94
|
+
select user_id, max(last_seen_at) as last_seen from ${table} group by user_id`);
|
|
95
|
+
return new Map(result.rows.map((row) => [row.user_id, row.last_seen]));
|
|
96
|
+
},
|
|
97
|
+
revokeAllExcept: async (userId, keepSessionId) => {
|
|
98
|
+
const result = await db.query(sql `update ${table} set revoked = ${true} where user_id = ${userId} and id != ${keepSessionId} and revoked = ${false}`);
|
|
99
|
+
return result.rowsAffected;
|
|
100
|
+
},
|
|
80
101
|
};
|
|
81
102
|
}
|
|
82
103
|
//# sourceMappingURL=sessions.js.map
|
package/dist/sessions.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sessions.js","sourceRoot":"","sources":["../src/sessions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACrD,OAAO,EAAuB,UAAU,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,eAAe,CAAA;AAC3E,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"sessions.js","sourceRoot":"","sources":["../src/sessions.ts"],"names":[],"mappings":"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"}
|
package/dist/store.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { DatabaseHandle } from '@cogenta/core';
|
|
|
2
2
|
import type { CollectionDefinition } from '@cogenta/schema';
|
|
3
3
|
import { createApiKeyStore } from './api-keys.js';
|
|
4
4
|
import { createAuditLog } from './audit.js';
|
|
5
|
+
import { createAuditIntegrityStore } from './audit-integrity.js';
|
|
5
6
|
import { createCredentialStore } from './credentials.js';
|
|
6
7
|
import { createAuthService } from './login.js';
|
|
7
8
|
import { createRateLimiter } from './rate-limit.js';
|
|
@@ -25,6 +26,8 @@ export interface AuthStore {
|
|
|
25
26
|
readonly credentials: ReturnType<typeof createCredentialStore>;
|
|
26
27
|
readonly sessions: ReturnType<typeof createSessionStore>;
|
|
27
28
|
readonly audit: ReturnType<typeof createAuditLog>;
|
|
29
|
+
/** The scheduled counterpart to `audit.verify()` — fiche 21 task 3. */
|
|
30
|
+
readonly auditIntegrity: ReturnType<typeof createAuditIntegrityStore>;
|
|
28
31
|
readonly rateLimit: ReturnType<typeof createRateLimiter>;
|
|
29
32
|
readonly login: ReturnType<typeof createAuthService>;
|
|
30
33
|
/** Self-service "forgot password" tokens (L13's HTTP route, `resets.ts`). */
|
package/dist/store.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AACnD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAC3C,OAAO,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAA;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAC9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AACnD,OAAO,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAA;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAA;AAElD,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAC5C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAEnD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,EAAE,cAAc,CAAA;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,WAAW,EAAE,SAAS,oBAAoB,EAAE,CAAA;IACrD,sFAAsF;IACtF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,qCAAqC;IACrC,QAAQ,CAAC,QAAQ,CAAC,EAAE,cAAc,CAAA;IAClC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CAC5B;AAED,0EAA0E;AAC1E,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,OAAO,eAAe,CAAC,CAAA;IAClD,QAAQ,CAAC,WAAW,EAAE,UAAU,CAAC,OAAO,qBAAqB,CAAC,CAAA;IAC9D,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAA;IACxD,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,OAAO,cAAc,CAAC,CAAA;IACjD,QAAQ,CAAC,SAAS,EAAE,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAA;IACxD,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAA;IACpD,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC,OAAO,wBAAwB,CAAC,CAAA;IAC5D,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAA;CACvD;AAED,wBAAsB,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,SAAS,CAAC,
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AACnD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAC3C,OAAO,EAAE,yBAAyB,EAAE,MAAM,sBAAsB,CAAA;AAChE,OAAO,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAA;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAC9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AACnD,OAAO,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAA;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAA;AAElD,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAC5C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAEnD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,EAAE,cAAc,CAAA;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,WAAW,EAAE,SAAS,oBAAoB,EAAE,CAAA;IACrD,sFAAsF;IACtF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,qCAAqC;IACrC,QAAQ,CAAC,QAAQ,CAAC,EAAE,cAAc,CAAA;IAClC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CAC5B;AAED,0EAA0E;AAC1E,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,OAAO,eAAe,CAAC,CAAA;IAClD,QAAQ,CAAC,WAAW,EAAE,UAAU,CAAC,OAAO,qBAAqB,CAAC,CAAA;IAC9D,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAA;IACxD,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,OAAO,cAAc,CAAC,CAAA;IACjD,uEAAuE;IACvE,QAAQ,CAAC,cAAc,EAAE,UAAU,CAAC,OAAO,yBAAyB,CAAC,CAAA;IACrE,QAAQ,CAAC,SAAS,EAAE,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAA;IACxD,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAA;IACpD,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC,OAAO,wBAAwB,CAAC,CAAA;IAC5D,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAA;CACvD;AAED,wBAAsB,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,SAAS,CAAC,CAuBnF"}
|
package/dist/store.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createApiKeyStore } from './api-keys.js';
|
|
2
2
|
import { createAuditLog } from './audit.js';
|
|
3
|
+
import { createAuditIntegrityStore } from './audit-integrity.js';
|
|
3
4
|
import { createCredentialStore } from './credentials.js';
|
|
4
5
|
import { createAuthService } from './login.js';
|
|
5
6
|
import { createRateLimiter } from './rate-limit.js';
|
|
@@ -10,11 +11,13 @@ import { createUserStore } from './users.js';
|
|
|
10
11
|
export async function createAuthStore(options) {
|
|
11
12
|
await ensureAuthTables(options.db);
|
|
12
13
|
const now = options.now ?? Date.now;
|
|
14
|
+
const audit = createAuditLog(options.db, now);
|
|
13
15
|
return {
|
|
14
16
|
users: createUserStore(options.db, now),
|
|
15
17
|
credentials: createCredentialStore(options.db, now),
|
|
16
18
|
sessions: createSessionStore(options.db, now),
|
|
17
|
-
audit
|
|
19
|
+
audit,
|
|
20
|
+
auditIntegrity: createAuditIntegrityStore(options.db, audit, { now }),
|
|
18
21
|
rateLimit: createRateLimiter(options.db, now),
|
|
19
22
|
resets: createPasswordResetStore(options.db, now),
|
|
20
23
|
apiKeys: createApiKeyStore(options.db, now),
|
package/dist/store.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAC3C,OAAO,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAA;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAC9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AACnD,OAAO,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAA;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAA;AAClD,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAC3C,OAAO,EAAE,yBAAyB,EAAE,MAAM,sBAAsB,CAAA;AAChE,OAAO,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAA;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAC9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AACnD,OAAO,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAA;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAA;AAClD,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AA6B5C,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,OAAyB;IAC7D,MAAM,gBAAgB,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;IAClC,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAA;IACnC,MAAM,KAAK,GAAG,cAAc,CAAC,OAAO,CAAC,EAAE,EAAE,GAAG,CAAC,CAAA;IAE7C,OAAO;QACL,KAAK,EAAE,eAAe,CAAC,OAAO,CAAC,EAAE,EAAE,GAAG,CAAC;QACvC,WAAW,EAAE,qBAAqB,CAAC,OAAO,CAAC,EAAE,EAAE,GAAG,CAAC;QACnD,QAAQ,EAAE,kBAAkB,CAAC,OAAO,CAAC,EAAE,EAAE,GAAG,CAAC;QAC7C,KAAK;QACL,cAAc,EAAE,yBAAyB,CAAC,OAAO,CAAC,EAAE,EAAE,KAAK,EAAE,EAAE,GAAG,EAAE,CAAC;QACrE,SAAS,EAAE,iBAAiB,CAAC,OAAO,CAAC,EAAE,EAAE,GAAG,CAAC;QAC7C,MAAM,EAAE,wBAAwB,CAAC,OAAO,CAAC,EAAE,EAAE,GAAG,CAAC;QACjD,OAAO,EAAE,iBAAiB,CAAC,OAAO,CAAC,EAAE,EAAE,GAAG,CAAC;QAC3C,KAAK,EAAE,iBAAiB,CAAC;YACvB,EAAE,EAAE,OAAO,CAAC,EAAE;YACd,UAAU,EAAE,OAAO,CAAC,UAAU;YAC9B,WAAW,EAAE,OAAO,CAAC,WAAW;YAChC,GAAG;YACH,GAAG,CAAC,OAAO,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC;YACnE,GAAG,CAAC,OAAO,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC;SAC1E,CAAC;KACH,CAAA;AACH,CAAC"}
|
package/dist/tables.d.ts
CHANGED
|
@@ -7,6 +7,23 @@ export declare const TABLES: {
|
|
|
7
7
|
readonly passwordResets: 'cogenta_password_resets';
|
|
8
8
|
readonly auditLog: 'cogenta_audit_log';
|
|
9
9
|
readonly apiKeys: 'cogenta_api_keys';
|
|
10
|
+
/** Aggregated per-day call counts (fiche 20 task 4) — never a line-per-call log. */
|
|
11
|
+
readonly apiKeyUsage: 'cogenta_api_key_usage';
|
|
12
|
+
/**
|
|
13
|
+
* Fiche 21 task 5: a singleton row naming the last entry a `prune()` call
|
|
14
|
+
* removed. Never touched by anything but `prune()` and read by
|
|
15
|
+
* `verify()`/`verifyRange()`, which trust it as the chain's new starting
|
|
16
|
+
* point instead of requiring `previousHash: null` — a truncated chain's
|
|
17
|
+
* oldest surviving row legitimately chains to a hash that is no longer in
|
|
18
|
+
* the table.
|
|
19
|
+
*/
|
|
20
|
+
readonly auditGenesis: 'cogenta_audit_genesis';
|
|
21
|
+
/**
|
|
22
|
+
* Fiche 21 task 3: the one row a scheduled integrity check reads and
|
|
23
|
+
* writes — last outcome, last checkpoint, whether the chain is currently
|
|
24
|
+
* believed broken. Owned by `createAuditIntegrityStore` (`audit-integrity.ts`).
|
|
25
|
+
*/
|
|
26
|
+
readonly auditIntegrity: 'cogenta_audit_integrity';
|
|
10
27
|
};
|
|
11
28
|
/**
|
|
12
29
|
* Every table this package owns.
|
package/dist/tables.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tables.d.ts","sourceRoot":"","sources":["../src/tables.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,cAAc,EAKpB,MAAM,eAAe,CAAA;AAEtB,eAAO,MAAM,MAAM;aACjB,KAAK,EAAE,eAAe;aACtB,WAAW,EAAE,qBAAqB;aAClC,QAAQ,EAAE,kBAAkB;aAC5B,aAAa,EAAE,wBAAwB;aACvC,cAAc,EAAE,yBAAyB;aACzC,QAAQ,EAAE,mBAAmB;aAC7B,OAAO,EAAE,kBAAkB;
|
|
1
|
+
{"version":3,"file":"tables.d.ts","sourceRoot":"","sources":["../src/tables.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,cAAc,EAKpB,MAAM,eAAe,CAAA;AAEtB,eAAO,MAAM,MAAM;aACjB,KAAK,EAAE,eAAe;aACtB,WAAW,EAAE,qBAAqB;aAClC,QAAQ,EAAE,kBAAkB;aAC5B,aAAa,EAAE,wBAAwB;aACvC,cAAc,EAAE,yBAAyB;aACzC,QAAQ,EAAE,mBAAmB;aAC7B,OAAO,EAAE,kBAAkB;IAC3B,oFAAoF;aACpF,WAAW,EAAE,uBAAuB;IACpC;;;;;;;OAOG;aACH,YAAY,EAAE,uBAAuB;IACrC;;;;OAIG;aACH,cAAc,EAAE,yBAAyB;CACjC,CAAA;AAWV;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CAAC,EAAE,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAuOxE"}
|
package/dist/tables.js
CHANGED
|
@@ -7,6 +7,23 @@ export const TABLES = {
|
|
|
7
7
|
passwordResets: 'cogenta_password_resets',
|
|
8
8
|
auditLog: 'cogenta_audit_log',
|
|
9
9
|
apiKeys: 'cogenta_api_keys',
|
|
10
|
+
/** Aggregated per-day call counts (fiche 20 task 4) — never a line-per-call log. */
|
|
11
|
+
apiKeyUsage: 'cogenta_api_key_usage',
|
|
12
|
+
/**
|
|
13
|
+
* Fiche 21 task 5: a singleton row naming the last entry a `prune()` call
|
|
14
|
+
* removed. Never touched by anything but `prune()` and read by
|
|
15
|
+
* `verify()`/`verifyRange()`, which trust it as the chain's new starting
|
|
16
|
+
* point instead of requiring `previousHash: null` — a truncated chain's
|
|
17
|
+
* oldest surviving row legitimately chains to a hash that is no longer in
|
|
18
|
+
* the table.
|
|
19
|
+
*/
|
|
20
|
+
auditGenesis: 'cogenta_audit_genesis',
|
|
21
|
+
/**
|
|
22
|
+
* Fiche 21 task 3: the one row a scheduled integrity check reads and
|
|
23
|
+
* writes — last outcome, last checkpoint, whether the chain is currently
|
|
24
|
+
* believed broken. Owned by `createAuditIntegrityStore` (`audit-integrity.ts`).
|
|
25
|
+
*/
|
|
26
|
+
auditIntegrity: 'cogenta_audit_integrity',
|
|
10
27
|
};
|
|
11
28
|
/** `varchar` on Postgres/MySQL, `text` on SQLite — encapsulated once, here. */
|
|
12
29
|
function textColumn(dialect, length) {
|
|
@@ -31,6 +48,9 @@ export async function ensureAuthTables(db) {
|
|
|
31
48
|
const passwordResets = identifier(TABLES.passwordResets, d);
|
|
32
49
|
const auditLog = identifier(TABLES.auditLog, d);
|
|
33
50
|
const apiKeys = identifier(TABLES.apiKeys, d);
|
|
51
|
+
const apiKeyUsage = identifier(TABLES.apiKeyUsage, d);
|
|
52
|
+
const auditGenesis = identifier(TABLES.auditGenesis, d);
|
|
53
|
+
const auditIntegrity = identifier(TABLES.auditIntegrity, d);
|
|
34
54
|
const t512 = textColumn(d, 512);
|
|
35
55
|
const t255 = textColumn(d, 255);
|
|
36
56
|
const t64 = textColumn(d, 64);
|
|
@@ -48,6 +68,14 @@ export async function ensureAuthTables(db) {
|
|
|
48
68
|
created_at ${t64} not null,
|
|
49
69
|
updated_at ${t64} not null
|
|
50
70
|
)`);
|
|
71
|
+
// Public profile (fiche 17 task 3), added to a table that may already exist
|
|
72
|
+
// on an upgraded site — same additive pattern as the API key columns below.
|
|
73
|
+
// All four are nullable: "not set" is the default for every account that
|
|
74
|
+
// predates this column, not an empty string to backfill.
|
|
75
|
+
await addColumnIfMissing(db, users, 'display_name', t255);
|
|
76
|
+
await addColumnIfMissing(db, users, 'avatar_media_id', t64);
|
|
77
|
+
await addColumnIfMissing(db, users, 'bio', sql `text`);
|
|
78
|
+
await addColumnIfMissing(db, users, 'locale', t64);
|
|
51
79
|
await db.query(sql `
|
|
52
80
|
create table if not exists ${credentials} (
|
|
53
81
|
id ${t64} not null primary key,
|
|
@@ -108,6 +136,19 @@ export async function ensureAuthTables(db) {
|
|
|
108
136
|
hash ${t64} not null,
|
|
109
137
|
previous_hash ${t64}
|
|
110
138
|
)`);
|
|
139
|
+
// Fiche 21 task 1: which content version this entry's action produced, so
|
|
140
|
+
// its detail view can ask `GET .../diff?from={version-1}&to={version}`
|
|
141
|
+
// instead of re-deriving one. Deliberately **not** part of `computeHash`'s
|
|
142
|
+
// canonical fields (see `audit.ts`) — adding a column to the hash would
|
|
143
|
+
// change the hash of every entry a site already has, and `verify()` would
|
|
144
|
+
// call its own untouched history broken the moment this code runs. A
|
|
145
|
+
// plain `alter table` rather than a place in the `create table` above,
|
|
146
|
+
// because that statement only runs for a table that does not exist yet —
|
|
147
|
+
// an upgrade's `cogenta_audit_log` already does, and would never see the
|
|
148
|
+
// column otherwise. No portable `add column if not exists` across all
|
|
149
|
+
// three dialects (SQLite has none at all), so — like `createIndexIfMissing`
|
|
150
|
+
// just below — this is a `try`, not a check.
|
|
151
|
+
await db.query(sql `alter table ${auditLog} add column version ${t64}`).catch(() => undefined);
|
|
111
152
|
await db.query(sql `
|
|
112
153
|
create table if not exists ${apiKeys} (
|
|
113
154
|
id ${t64} not null primary key,
|
|
@@ -128,16 +169,103 @@ export async function ensureAuthTables(db) {
|
|
|
128
169
|
created_at ${t64} not null,
|
|
129
170
|
expires_at ${t64},
|
|
130
171
|
revoked_at ${t64},
|
|
131
|
-
last_used_at ${t64}
|
|
172
|
+
last_used_at ${t64},
|
|
173
|
+
-- Requests per minute this key may make (fiche 20 task 3). Nullable so
|
|
174
|
+
-- a row written before this column existed still parses: fromRow()
|
|
175
|
+
-- treats null as "the default quota", the same courtesy expires_at
|
|
176
|
+
-- already gets for "never".
|
|
177
|
+
rate_limit_per_minute integer,
|
|
178
|
+
-- Set when this key was replaced by a rotation (fiche 20 task 2): the
|
|
179
|
+
-- id of the new key. expires_at is what actually ends this key's
|
|
180
|
+
-- grace window — this column only makes that window visible as
|
|
181
|
+
-- "superseded", rather than indistinguishable from an ordinary expiry.
|
|
182
|
+
superseded_by ${t64}
|
|
132
183
|
)`);
|
|
184
|
+
// Both columns above are additive for a database that already had this
|
|
185
|
+
// table before fiche 20: `create table if not exists` never touches an
|
|
186
|
+
// existing table, so an upgrade needs its own statement. There is no
|
|
187
|
+
// portable "add column if not exists" (the same reason indexes below use
|
|
188
|
+
// catch-and-ignore) — the failure this swallows is exactly "already there".
|
|
189
|
+
await addColumnIfMissing(db, apiKeys, 'rate_limit_per_minute', sql `integer`);
|
|
190
|
+
await addColumnIfMissing(db, apiKeys, 'superseded_by', sql `${t64}`);
|
|
191
|
+
await db.query(sql `
|
|
192
|
+
create table if not exists ${apiKeyUsage} (
|
|
193
|
+
id ${t64} not null primary key,
|
|
194
|
+
key_id ${t64} not null,
|
|
195
|
+
-- One row per key per UTC calendar day, incremented on every
|
|
196
|
+
-- successful verification. Aggregated by construction — a line per
|
|
197
|
+
-- call would be an absurd volume for a key doing hundreds of requests
|
|
198
|
+
-- a minute (fiche 20 task 4).
|
|
199
|
+
day ${textColumn(d, 10)} not null,
|
|
200
|
+
count integer not null
|
|
201
|
+
)`);
|
|
202
|
+
// Fiche 21 task 5: at most one row, id `'singleton'`. Absent means no
|
|
203
|
+
// `prune()` has ever run, which `verify()`/`verifyRange()` read as "the
|
|
204
|
+
// chain starts at its very first entry" — the same behaviour as before
|
|
205
|
+
// this table existed.
|
|
206
|
+
await db.query(sql `
|
|
207
|
+
create table if not exists ${auditGenesis} (
|
|
208
|
+
id ${t64} not null primary key,
|
|
209
|
+
entry_id ${t64} not null,
|
|
210
|
+
entry_at ${t64} not null,
|
|
211
|
+
entry_hash ${t64} not null,
|
|
212
|
+
pruned_count ${t64} not null,
|
|
213
|
+
pruned_at ${t64} not null
|
|
214
|
+
)`);
|
|
215
|
+
// Fiche 21 task 3: at most one row, id `'singleton'` — the scheduled
|
|
216
|
+
// integrity check's memory of what it last found, surviving a restart the
|
|
217
|
+
// same way the scheduled-publication tick's own state does not need to
|
|
218
|
+
// (that one replays from the content table itself; this one would
|
|
219
|
+
// otherwise have to replay the whole log on every restart).
|
|
220
|
+
await db.query(sql `
|
|
221
|
+
create table if not exists ${auditIntegrity} (
|
|
222
|
+
id ${t64} not null primary key,
|
|
223
|
+
state ${t64} not null,
|
|
224
|
+
checkpoint_id ${t64},
|
|
225
|
+
checkpoint_at ${t64},
|
|
226
|
+
checkpoint_hash ${t64},
|
|
227
|
+
entries_checked ${t64},
|
|
228
|
+
last_checked_at ${t64},
|
|
229
|
+
last_mode ${t64},
|
|
230
|
+
last_full_checked_at ${t64},
|
|
231
|
+
broken_at ${t64},
|
|
232
|
+
broken_entry_id ${t64},
|
|
233
|
+
broken_message text
|
|
234
|
+
)`);
|
|
235
|
+
// Session device metadata (fiche 18 task 2): additive for a database that
|
|
236
|
+
// already had this table before this fiche — there is no portable "add
|
|
237
|
+
// column if not exists", so the failure this swallows is exactly "already
|
|
238
|
+
// there", the same reasoning `createIndexIfMissing` below already follows.
|
|
239
|
+
// Never the raw `User-Agent` (see `user-agent.ts`): only its two-word
|
|
240
|
+
// distillation is ever written to either column.
|
|
241
|
+
await addColumnIfMissing(db, sessions, 'browser', t64);
|
|
242
|
+
await addColumnIfMissing(db, sessions, 'device', t64);
|
|
133
243
|
await createIndexIfMissing(db, 'cogenta_credentials_user', credentials, sql `(user_id)`);
|
|
134
244
|
await createIndexIfMissing(db, 'cogenta_sessions_user', sessions, sql `(user_id)`);
|
|
135
245
|
await createIndexIfMissing(db, 'cogenta_login_attempts_subject_at', loginAttempts, sql `(subject, at)`);
|
|
136
246
|
await createIndexIfMissing(db, 'cogenta_password_resets_user', passwordResets, sql `(user_id)`);
|
|
247
|
+
await createUniqueIndexIfMissing(db, 'cogenta_api_key_usage_key_day', apiKeyUsage, sql `(key_id, day)`);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Additive column for a table that may already exist on an upgraded site
|
|
251
|
+
* (fiche 20 task 3's rate-limit/rotation columns, fiche 17 task 3's profile
|
|
252
|
+
* fields, fiche 18 task 2's session device metadata): same catch-and-ignore
|
|
253
|
+
* shape as `createIndexIfMissing` below, for the same reason — no dialect
|
|
254
|
+
* here has a portable "add column if not exists".
|
|
255
|
+
*/
|
|
256
|
+
async function addColumnIfMissing(db, table, column, type) {
|
|
257
|
+
await db
|
|
258
|
+
.query(sql `alter table ${table} add column ${identifier(column, db.dialect)} ${type}`)
|
|
259
|
+
.catch(() => undefined); // already there — no portable "add column if not exists"
|
|
137
260
|
}
|
|
138
261
|
async function createIndexIfMissing(db, name, table, columns) {
|
|
139
262
|
await db
|
|
140
263
|
.query(sql `create index ${identifier(name, db.dialect)} on ${table} ${columns}`)
|
|
141
264
|
.catch(() => undefined); // already there — no portable "if not exists" for indexes
|
|
142
265
|
}
|
|
266
|
+
async function createUniqueIndexIfMissing(db, name, table, columns) {
|
|
267
|
+
await db
|
|
268
|
+
.query(sql `create unique index ${identifier(name, db.dialect)} on ${table} ${columns}`)
|
|
269
|
+
.catch(() => undefined); // already there — no portable "if not exists" for indexes
|
|
270
|
+
}
|
|
143
271
|
//# sourceMappingURL=tables.js.map
|
package/dist/tables.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tables.js","sourceRoot":"","sources":["../src/tables.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,UAAU,EAEV,GAAG,EACH,SAAS,GACV,MAAM,eAAe,CAAA;AAEtB,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,KAAK,EAAE,eAAe;IACtB,WAAW,EAAE,qBAAqB;IAClC,QAAQ,EAAE,kBAAkB;IAC5B,aAAa,EAAE,wBAAwB;IACvC,cAAc,EAAE,yBAAyB;IACzC,QAAQ,EAAE,mBAAmB;IAC7B,OAAO,EAAE,kBAAkB;
|
|
1
|
+
{"version":3,"file":"tables.js","sourceRoot":"","sources":["../src/tables.ts"],"names":[],"mappings":"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"}
|