@micacms/auth-oidc 0.2.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/admission.d.ts +5 -4
- package/dist/admission.d.ts.map +1 -1
- package/dist/admission.js +22 -15
- package/dist/admission.js.map +1 -1
- package/dist/config.d.ts +8 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +41 -4
- package/dist/config.js.map +1 -1
- package/dist/gate.d.ts +4 -5
- package/dist/gate.d.ts.map +1 -1
- package/dist/gate.js +14 -13
- package/dist/gate.js.map +1 -1
- package/dist/index.d.ts +4 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -11
- package/dist/index.js.map +1 -1
- package/dist/oidc.d.ts +9 -2
- package/dist/oidc.d.ts.map +1 -1
- package/dist/oidc.js +22 -9
- package/dist/oidc.js.map +1 -1
- package/dist/return-to.d.ts +8 -0
- package/dist/return-to.d.ts.map +1 -0
- package/dist/return-to.js +24 -0
- package/dist/return-to.js.map +1 -0
- package/dist/routes.d.ts +2 -2
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +33 -23
- package/dist/routes.js.map +1 -1
- package/dist/session.d.ts +58 -26
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +137 -72
- package/dist/session.js.map +1 -1
- package/package.json +4 -4
- package/dist/sessions-repository.d.ts +0 -56
- package/dist/sessions-repository.d.ts.map +0 -1
- package/dist/sessions-repository.js +0 -96
- package/dist/sessions-repository.js.map +0 -1
package/dist/session.js
CHANGED
|
@@ -1,31 +1,51 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Session management.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The session is the cookie: who the provider said this is, a CSRF token and
|
|
5
|
+
* an expiry, encrypted with a key derived from SESSION_SECRET. There is no
|
|
6
|
+
* session table, so Mica stores nothing about people at all.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* - AES-256-GCM, so the cookie can be neither read nor altered. Encrypted and
|
|
9
|
+
* not only signed, because it carries an email address and groups.
|
|
10
|
+
* - A fresh random IV per cookie, and a new cookie at every sign-in, so a
|
|
11
|
+
* client-supplied value is never adopted (session fixation).
|
|
12
|
+
* - The idle window slides: an active session is re-issued every few
|
|
13
|
+
* minutes with a later expiry, capped by the absolute lifetime.
|
|
13
14
|
*
|
|
14
|
-
*
|
|
15
|
-
* a
|
|
15
|
+
* What this gives up is server-side revocation of one cookie. Sign-out clears
|
|
16
|
+
* it in the browser; a copy taken before then stays valid until it expires.
|
|
17
|
+
* Rotating SESSION_SECRET ends every session at once. The admission rules are
|
|
18
|
+
* still re-read on every request — see gate.ts and docs/decisions.md.
|
|
16
19
|
*/
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
+
import { createCipheriv, createDecipheriv, hkdfSync, randomBytes, timingSafeEqual } from "node:crypto";
|
|
21
|
+
import { setCookie } from "hono/cookie";
|
|
22
|
+
import { randomToken } from "@micacms/runtime";
|
|
20
23
|
export const SESSION_COOKIE = "mica_session";
|
|
21
24
|
export const CSRF_COOKIE = "mica_csrf";
|
|
22
25
|
export const CSRF_HEADER = "x-csrf-token";
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
export const TRANSACTION_COOKIE = "mica_oidc";
|
|
27
|
+
/**
|
|
28
|
+
* With https, the names carry a prefix the browser enforces: `__Host-` means
|
|
29
|
+
* Secure, path `/` and no Domain, so a sibling subdomain cannot plant or
|
|
30
|
+
* overwrite the cookie. The transaction cookie is scoped to `/api/auth`, so it
|
|
31
|
+
* can only be `__Secure-`. Over plain HTTP the cookies are not Secure, and a
|
|
32
|
+
* browser would refuse a prefixed name.
|
|
33
|
+
*/
|
|
34
|
+
export function cookieNames(secure) {
|
|
35
|
+
return secure
|
|
36
|
+
? {
|
|
37
|
+
session: `__Host-${SESSION_COOKIE}`,
|
|
38
|
+
csrf: `__Host-${CSRF_COOKIE}`,
|
|
39
|
+
transaction: `__Secure-${TRANSACTION_COOKIE}`,
|
|
40
|
+
}
|
|
41
|
+
: { session: SESSION_COOKIE, csrf: CSRF_COOKIE, transaction: TRANSACTION_COOKIE };
|
|
28
42
|
}
|
|
43
|
+
/** How often an active session is re-issued with a later idle expiry. */
|
|
44
|
+
const REFRESH_INTERVAL_MS = 5 * 60 * 1000;
|
|
45
|
+
/** Browsers drop cookies over 4096 bytes; refuse rather than sign in to nothing. */
|
|
46
|
+
const MAX_COOKIE_BYTES = 4000;
|
|
47
|
+
const IV_BYTES = 12;
|
|
48
|
+
const TAG_BYTES = 16;
|
|
29
49
|
/** Constant-time string comparison that tolerates differing lengths. */
|
|
30
50
|
export function safeEqual(a, b) {
|
|
31
51
|
const left = Buffer.from(a);
|
|
@@ -37,64 +57,118 @@ export function safeEqual(a, b) {
|
|
|
37
57
|
}
|
|
38
58
|
return timingSafeEqual(left, right);
|
|
39
59
|
}
|
|
40
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Keycloak sends `/mica-editors` where the rule says `mica-editors`; both
|
|
62
|
+
* spellings are the same group — see admission.ts.
|
|
63
|
+
*/
|
|
64
|
+
const groupName = (group) => group.replace(/^\//, "");
|
|
65
|
+
/** Defensive read of a decrypted payload: it was written by us, but checked anyway. */
|
|
66
|
+
function parseRecord(json) {
|
|
67
|
+
try {
|
|
68
|
+
const value = JSON.parse(json);
|
|
69
|
+
const identity = value.identity;
|
|
70
|
+
if (!identity ||
|
|
71
|
+
typeof identity.issuer !== "string" ||
|
|
72
|
+
typeof identity.subject !== "string" ||
|
|
73
|
+
typeof value.csrfToken !== "string" ||
|
|
74
|
+
typeof value.createdAt !== "string" ||
|
|
75
|
+
typeof value.refreshedAt !== "string" ||
|
|
76
|
+
typeof value.expiresAt !== "string") {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
identity: {
|
|
81
|
+
issuer: identity.issuer,
|
|
82
|
+
subject: identity.subject,
|
|
83
|
+
email: typeof identity.email === "string" ? identity.email : "",
|
|
84
|
+
emailVerified: identity.emailVerified === true,
|
|
85
|
+
name: typeof identity.name === "string" ? identity.name : "",
|
|
86
|
+
groups: Array.isArray(identity.groups)
|
|
87
|
+
? identity.groups.filter((group) => typeof group === "string")
|
|
88
|
+
: [],
|
|
89
|
+
},
|
|
90
|
+
csrfToken: value.csrfToken,
|
|
91
|
+
createdAt: value.createdAt,
|
|
92
|
+
refreshedAt: value.refreshedAt,
|
|
93
|
+
expiresAt: value.expiresAt,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export function createSessionService(config) {
|
|
41
101
|
const ttlMs = config.SESSION_TTL_HOURS * 3600_000;
|
|
42
102
|
const idleMs = config.SESSION_IDLE_HOURS * 3600_000;
|
|
103
|
+
const key = Buffer.from(hkdfSync("sha256", config.sessionSecret, "", "mica session cookie v1", 32));
|
|
104
|
+
// Only the groups a rule names are kept: an Entra token can carry hundreds,
|
|
105
|
+
// and a cookie holds 4 KB. A group added to the rules later counts from the
|
|
106
|
+
// next sign-in.
|
|
107
|
+
const admitted = new Set(config.admitGroups.map(groupName));
|
|
43
108
|
/** Expiry is the earlier of the idle window and the absolute lifetime. */
|
|
44
109
|
const nextExpiry = (createdAt, now) => new Date(Math.min(now + idleMs, createdAt + ttlMs)).toISOString();
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
110
|
+
const seal = (record) => {
|
|
111
|
+
const iv = randomBytes(IV_BYTES);
|
|
112
|
+
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
113
|
+
const body = Buffer.concat([cipher.update(JSON.stringify(record), "utf8"), cipher.final()]);
|
|
114
|
+
const cookie = Buffer.concat([iv, body, cipher.getAuthTag()]).toString("base64url");
|
|
115
|
+
if (cookie.length > MAX_COOKIE_BYTES) {
|
|
116
|
+
throw new Error("The session does not fit in a cookie; the identity is too large.");
|
|
117
|
+
}
|
|
118
|
+
return cookie;
|
|
119
|
+
};
|
|
120
|
+
const open = (cookieValue) => {
|
|
121
|
+
const raw = Buffer.from(cookieValue, "base64url");
|
|
122
|
+
if (raw.length <= IV_BYTES + TAG_BYTES)
|
|
50
123
|
return null;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
124
|
+
try {
|
|
125
|
+
const decipher = createDecipheriv("aes-256-gcm", key, raw.subarray(0, IV_BYTES));
|
|
126
|
+
decipher.setAuthTag(raw.subarray(raw.length - TAG_BYTES));
|
|
127
|
+
const json = Buffer.concat([
|
|
128
|
+
decipher.update(raw.subarray(IV_BYTES, raw.length - TAG_BYTES)),
|
|
129
|
+
decipher.final(),
|
|
130
|
+
]).toString("utf8");
|
|
131
|
+
return parseRecord(json);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// Forged, truncated, or sealed with a secret that has since rotated.
|
|
54
135
|
return null;
|
|
55
|
-
|
|
136
|
+
}
|
|
56
137
|
};
|
|
57
138
|
return {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const csrfToken = randomToken(32);
|
|
139
|
+
names: cookieNames(config.cookieSecure),
|
|
140
|
+
create(identity) {
|
|
61
141
|
const now = Date.now();
|
|
62
|
-
const record =
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
142
|
+
const record = {
|
|
143
|
+
identity: {
|
|
144
|
+
...identity,
|
|
145
|
+
groups: identity.groups.filter((group) => admitted.has(groupName(group))),
|
|
146
|
+
},
|
|
147
|
+
csrfToken: randomToken(32),
|
|
148
|
+
createdAt: new Date(now).toISOString(),
|
|
149
|
+
refreshedAt: new Date(now).toISOString(),
|
|
66
150
|
expiresAt: nextExpiry(now, now),
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
});
|
|
70
|
-
return { record, cookie: `${token}.${sign(token, config.sessionSecret)}`, csrfToken };
|
|
151
|
+
};
|
|
152
|
+
return { record, cookie: seal(record) };
|
|
71
153
|
},
|
|
72
154
|
resolve(cookieValue) {
|
|
73
|
-
|
|
74
|
-
if (!token)
|
|
155
|
+
if (!cookieValue)
|
|
75
156
|
return null;
|
|
76
|
-
const record =
|
|
77
|
-
if (!record)
|
|
157
|
+
const record = open(cookieValue);
|
|
158
|
+
if (!record || Date.parse(record.expiresAt) <= Date.now())
|
|
78
159
|
return null;
|
|
79
|
-
const now = Date.now();
|
|
80
|
-
if (Date.parse(record.expiresAt) <= now) {
|
|
81
|
-
sessions.delete(record.id);
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
if (now - Date.parse(record.lastSeenAt) > TOUCH_INTERVAL_MS) {
|
|
85
|
-
const expiresAt = nextExpiry(Date.parse(record.createdAt), now);
|
|
86
|
-
sessions.touch(record.id, new Date(now).toISOString(), expiresAt);
|
|
87
|
-
return { ...record, lastSeenAt: new Date(now).toISOString(), expiresAt };
|
|
88
|
-
}
|
|
89
160
|
return record;
|
|
90
161
|
},
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
if (
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
162
|
+
refresh(record) {
|
|
163
|
+
const now = Date.now();
|
|
164
|
+
if (now - Date.parse(record.refreshedAt) < REFRESH_INTERVAL_MS)
|
|
165
|
+
return null;
|
|
166
|
+
const next = {
|
|
167
|
+
...record,
|
|
168
|
+
refreshedAt: new Date(now).toISOString(),
|
|
169
|
+
expiresAt: nextExpiry(Date.parse(record.createdAt), now),
|
|
170
|
+
};
|
|
171
|
+
return { record: next, cookie: seal(next) };
|
|
98
172
|
},
|
|
99
173
|
sessionCookieOptions() {
|
|
100
174
|
return {
|
|
@@ -127,9 +201,6 @@ export function createSessionService(sessions, config) {
|
|
|
127
201
|
maxAge: 0,
|
|
128
202
|
};
|
|
129
203
|
},
|
|
130
|
-
purgeExpired() {
|
|
131
|
-
return sessions.deleteExpired();
|
|
132
|
-
},
|
|
133
204
|
};
|
|
134
205
|
}
|
|
135
206
|
/* ------------------------------------------------------------------ *
|
|
@@ -137,14 +208,8 @@ export function createSessionService(sessions, config) {
|
|
|
137
208
|
* ------------------------------------------------------------------ */
|
|
138
209
|
/** Issues the session cookies for a browser that has just been identified. */
|
|
139
210
|
export function establishSession(c, sessions, identity) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
identity,
|
|
144
|
-
ip: clientIp(c),
|
|
145
|
-
userAgent: c.req.header("user-agent") ?? null,
|
|
146
|
-
});
|
|
147
|
-
setCookie(c, SESSION_COOKIE, cookie, sessions.sessionCookieOptions());
|
|
148
|
-
setCookie(c, CSRF_COOKIE, csrfToken, sessions.csrfCookieOptions());
|
|
211
|
+
const { record, cookie } = sessions.create(identity);
|
|
212
|
+
setCookie(c, sessions.names.session, cookie, sessions.sessionCookieOptions());
|
|
213
|
+
setCookie(c, sessions.names.csrf, record.csrfToken, sessions.csrfCookieOptions());
|
|
149
214
|
}
|
|
150
215
|
//# sourceMappingURL=session.js.map
|
package/dist/session.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@micacms/auth-oidc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The server-only OIDC gate for Mica: sign-in, sessions and admission around @micacms/runtime. Never enters a browser bundle, and never a dependency of the local editor.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,11 +25,11 @@
|
|
|
25
25
|
"dist"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"hono": "^4.
|
|
28
|
+
"hono": "^4.13.7",
|
|
29
29
|
"openid-client": "^6.8.5",
|
|
30
30
|
"zod": "^4.3.6",
|
|
31
|
-
"@micacms/
|
|
32
|
-
"@micacms/
|
|
31
|
+
"@micacms/runtime": "0.4.0",
|
|
32
|
+
"@micacms/core": "0.4.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@types/node": "^22.20.1",
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Session repository.
|
|
3
|
-
*
|
|
4
|
-
* The primary key is the SHA-256 of the cookie token, never the token itself:
|
|
5
|
-
* a leaked database backup therefore does not hand an attacker usable sessions.
|
|
6
|
-
*
|
|
7
|
-
* A row carries who the provider said this is, because there is no user table
|
|
8
|
-
* to look it up in. That is the whole of Mica's identity storage.
|
|
9
|
-
*/
|
|
10
|
-
import type { SqlDatabase } from "@micacms/runtime";
|
|
11
|
-
/**
|
|
12
|
-
* What the identity provider stated about the person holding this session.
|
|
13
|
-
*
|
|
14
|
-
* `null` where there is no provider: local mode has no user, and the row is
|
|
15
|
-
* then a CSRF token with an expiry.
|
|
16
|
-
*/
|
|
17
|
-
export interface SessionIdentity {
|
|
18
|
-
issuer: string;
|
|
19
|
-
/** Opaque and stable at that issuer. The account key, and never the email. */
|
|
20
|
-
subject: string;
|
|
21
|
-
email: string;
|
|
22
|
-
/** The provider's own `email_verified`. Absent counts as false. */
|
|
23
|
-
emailVerified: boolean;
|
|
24
|
-
name: string;
|
|
25
|
-
/** The groups claim as it arrived, or empty when the provider sends none. */
|
|
26
|
-
groups: string[];
|
|
27
|
-
}
|
|
28
|
-
export interface SessionRecord {
|
|
29
|
-
id: string;
|
|
30
|
-
identity: SessionIdentity | null;
|
|
31
|
-
csrfToken: string;
|
|
32
|
-
createdAt: string;
|
|
33
|
-
lastSeenAt: string;
|
|
34
|
-
expiresAt: string;
|
|
35
|
-
ip: string | null;
|
|
36
|
-
userAgent: string | null;
|
|
37
|
-
}
|
|
38
|
-
export declare function createSessionRepository(db: SqlDatabase): {
|
|
39
|
-
create(input: {
|
|
40
|
-
id: string;
|
|
41
|
-
identity: SessionIdentity | null;
|
|
42
|
-
csrfToken: string;
|
|
43
|
-
expiresAt: string;
|
|
44
|
-
ip?: string | null;
|
|
45
|
-
userAgent?: string | null;
|
|
46
|
-
}): SessionRecord;
|
|
47
|
-
find(id: string): SessionRecord | undefined;
|
|
48
|
-
touch(id: string, lastSeenAt: string, expiresAt: string): void;
|
|
49
|
-
delete(id: string): boolean;
|
|
50
|
-
/** Every session this person holds, on every device. */
|
|
51
|
-
deleteForIdentity(issuer: string, subject: string): number;
|
|
52
|
-
deleteExpired(now?: string): number;
|
|
53
|
-
count(): number;
|
|
54
|
-
};
|
|
55
|
-
export type SessionRepository = ReturnType<typeof createSessionRepository>;
|
|
56
|
-
//# sourceMappingURL=sessions-repository.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"sessions-repository.d.ts","sourceRoot":"","sources":["../src/sessions-repository.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,8EAA8E;IAC9E,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,mEAAmE;IACnE,aAAa,EAAE,OAAO,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,eAAe,GAAG,IAAI,CAAC;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AA2DD,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,WAAW;kBAErC;QACZ,EAAE,EAAE,MAAM,CAAC;QACX,QAAQ,EAAE,eAAe,GAAG,IAAI,CAAC;QACjC,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,EAAE,MAAM,CAAC;QAClB,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACnB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KAC3B,GAAG,aAAa;aAwBR,MAAM,GAAG,aAAa,GAAG,SAAS;cAKjC,MAAM,cAAc,MAAM,aAAa,MAAM,GAAG,IAAI;eAQnD,MAAM,GAAG,OAAO;IAI3B,wDAAwD;8BAC9B,MAAM,WAAW,MAAM,GAAG,MAAM;iCAKX,MAAM;aAI5C,MAAM;EAIlB;AAED,MAAM,MAAM,iBAAiB,GAAG,UAAU,CAAC,OAAO,uBAAuB,CAAC,CAAC"}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Session repository.
|
|
3
|
-
*
|
|
4
|
-
* The primary key is the SHA-256 of the cookie token, never the token itself:
|
|
5
|
-
* a leaked database backup therefore does not hand an attacker usable sessions.
|
|
6
|
-
*
|
|
7
|
-
* A row carries who the provider said this is, because there is no user table
|
|
8
|
-
* to look it up in. That is the whole of Mica's identity storage.
|
|
9
|
-
*/
|
|
10
|
-
/**
|
|
11
|
-
* The groups column holds whatever the provider sent, serialised. It is read
|
|
12
|
-
* back defensively — a claim that was a string, or an object, or absent, must
|
|
13
|
-
* not throw four weeks later on a request that has nothing to do with it.
|
|
14
|
-
*/
|
|
15
|
-
function parseGroups(value) {
|
|
16
|
-
try {
|
|
17
|
-
const parsed = JSON.parse(value);
|
|
18
|
-
return Array.isArray(parsed) ? parsed.filter((entry) => typeof entry === "string") : [];
|
|
19
|
-
}
|
|
20
|
-
catch {
|
|
21
|
-
return [];
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
function toRecord(row) {
|
|
25
|
-
return {
|
|
26
|
-
id: row.id,
|
|
27
|
-
identity: row.issuer && row.subject
|
|
28
|
-
? {
|
|
29
|
-
issuer: row.issuer,
|
|
30
|
-
subject: row.subject,
|
|
31
|
-
email: row.email ?? "",
|
|
32
|
-
emailVerified: row.email_verified === 1,
|
|
33
|
-
name: row.name ?? "",
|
|
34
|
-
groups: parseGroups(row.groups),
|
|
35
|
-
}
|
|
36
|
-
: null,
|
|
37
|
-
csrfToken: row.csrf_token,
|
|
38
|
-
createdAt: row.created_at,
|
|
39
|
-
lastSeenAt: row.last_seen_at,
|
|
40
|
-
expiresAt: row.expires_at,
|
|
41
|
-
ip: row.ip,
|
|
42
|
-
userAgent: row.user_agent,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
const COLUMNS = "id, issuer, subject, email, email_verified, name, groups, csrf_token, " +
|
|
46
|
-
"created_at, last_seen_at, expires_at, ip, user_agent";
|
|
47
|
-
export function createSessionRepository(db) {
|
|
48
|
-
return {
|
|
49
|
-
create(input) {
|
|
50
|
-
const now = new Date().toISOString();
|
|
51
|
-
db.run(`INSERT INTO sessions
|
|
52
|
-
(${COLUMNS}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
53
|
-
input.id,
|
|
54
|
-
input.identity?.issuer ?? null,
|
|
55
|
-
input.identity?.subject ?? null,
|
|
56
|
-
input.identity?.email ?? null,
|
|
57
|
-
input.identity?.emailVerified ? 1 : 0,
|
|
58
|
-
input.identity?.name ?? null,
|
|
59
|
-
JSON.stringify(input.identity?.groups ?? []),
|
|
60
|
-
input.csrfToken,
|
|
61
|
-
now,
|
|
62
|
-
now,
|
|
63
|
-
input.expiresAt,
|
|
64
|
-
input.ip ?? null,
|
|
65
|
-
input.userAgent ?? null,
|
|
66
|
-
]);
|
|
67
|
-
return this.find(input.id);
|
|
68
|
-
},
|
|
69
|
-
find(id) {
|
|
70
|
-
const row = db.get(`SELECT ${COLUMNS} FROM sessions WHERE id = ?`, [id]);
|
|
71
|
-
return row && toRecord(row);
|
|
72
|
-
},
|
|
73
|
-
touch(id, lastSeenAt, expiresAt) {
|
|
74
|
-
db.run("UPDATE sessions SET last_seen_at = ?, expires_at = ? WHERE id = ?", [
|
|
75
|
-
lastSeenAt,
|
|
76
|
-
expiresAt,
|
|
77
|
-
id,
|
|
78
|
-
]);
|
|
79
|
-
},
|
|
80
|
-
delete(id) {
|
|
81
|
-
return db.run("DELETE FROM sessions WHERE id = ?", [id]).changes > 0;
|
|
82
|
-
},
|
|
83
|
-
/** Every session this person holds, on every device. */
|
|
84
|
-
deleteForIdentity(issuer, subject) {
|
|
85
|
-
return db.run("DELETE FROM sessions WHERE issuer = ? AND subject = ?", [issuer, subject])
|
|
86
|
-
.changes;
|
|
87
|
-
},
|
|
88
|
-
deleteExpired(now = new Date().toISOString()) {
|
|
89
|
-
return db.run("DELETE FROM sessions WHERE expires_at <= ?", [now]).changes;
|
|
90
|
-
},
|
|
91
|
-
count() {
|
|
92
|
-
return db.get("SELECT COUNT(*) AS count FROM sessions")?.count ?? 0;
|
|
93
|
-
},
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
//# sourceMappingURL=sessions-repository.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"sessions-repository.js","sourceRoot":"","sources":["../src/sessions-repository.ts"],"names":[],"mappings":"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"}
|