@micacms/auth-oidc 0.2.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 +16 -0
- package/dist/admission.d.ts +43 -0
- package/dist/admission.d.ts.map +1 -0
- package/dist/admission.js +101 -0
- package/dist/admission.js.map +1 -0
- package/dist/config.d.ts +126 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +119 -0
- package/dist/config.js.map +1 -0
- package/dist/gate.d.ts +40 -0
- package/dist/gate.d.ts.map +1 -0
- package/dist/gate.js +84 -0
- package/dist/gate.js.map +1 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/oidc.d.ts +83 -0
- package/dist/oidc.d.ts.map +1 -0
- package/dist/oidc.js +224 -0
- package/dist/oidc.js.map +1 -0
- package/dist/routes.d.ts +29 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +135 -0
- package/dist/routes.js.map +1 -0
- package/dist/session.d.ts +59 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +150 -0
- package/dist/session.js.map +1 -0
- package/dist/sessions-repository.d.ts +56 -0
- package/dist/sessions-repository.d.ts.map +1 -0
- package/dist/sessions-repository.js +96 -0
- package/dist/sessions-repository.js.map +1 -0
- package/package.json +47 -0
package/dist/session.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session management.
|
|
3
|
+
*
|
|
4
|
+
* Server-side sessions in SQLite, addressed by an opaque random token held in a
|
|
5
|
+
* Secure/HttpOnly/SameSite cookie. No JWTs: revocation has to be immediate, and
|
|
6
|
+
* a row delete is the simplest way to get it.
|
|
7
|
+
*
|
|
8
|
+
* Three layers of defence on the cookie value:
|
|
9
|
+
* 1. the token is 256 bits from `crypto.randomBytes`;
|
|
10
|
+
* 2. it is HMAC-signed with SESSION_SECRET, so forged or truncated cookies
|
|
11
|
+
* are rejected without touching the database;
|
|
12
|
+
* 3. only its SHA-256 is stored, so a database dump yields no usable session.
|
|
13
|
+
*
|
|
14
|
+
* Session fixation is handled by never adopting a client-supplied identifier —
|
|
15
|
+
* a token is minted at login and the previous one deleted.
|
|
16
|
+
*/
|
|
17
|
+
import { createHash, createHmac, timingSafeEqual } from "node:crypto";
|
|
18
|
+
import { getCookie, setCookie } from "hono/cookie";
|
|
19
|
+
import { clientIp, randomToken } from "@micacms/runtime";
|
|
20
|
+
export const SESSION_COOKIE = "mica_session";
|
|
21
|
+
export const CSRF_COOKIE = "mica_csrf";
|
|
22
|
+
export const CSRF_HEADER = "x-csrf-token";
|
|
23
|
+
/** Writing `last_seen_at` on every request would be a write per page view. */
|
|
24
|
+
const TOUCH_INTERVAL_MS = 5 * 60 * 1000;
|
|
25
|
+
const sha256 = (value) => createHash("sha256").update(value).digest("hex");
|
|
26
|
+
function sign(token, secret) {
|
|
27
|
+
return createHmac("sha256", secret).update(token).digest("base64url");
|
|
28
|
+
}
|
|
29
|
+
/** Constant-time string comparison that tolerates differing lengths. */
|
|
30
|
+
export function safeEqual(a, b) {
|
|
31
|
+
const left = Buffer.from(a);
|
|
32
|
+
const right = Buffer.from(b);
|
|
33
|
+
if (left.length !== right.length) {
|
|
34
|
+
// Still compare, so the branch does not leak the length difference.
|
|
35
|
+
timingSafeEqual(left, left);
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
return timingSafeEqual(left, right);
|
|
39
|
+
}
|
|
40
|
+
export function createSessionService(sessions, config) {
|
|
41
|
+
const ttlMs = config.SESSION_TTL_HOURS * 3600_000;
|
|
42
|
+
const idleMs = config.SESSION_IDLE_HOURS * 3600_000;
|
|
43
|
+
/** Expiry is the earlier of the idle window and the absolute lifetime. */
|
|
44
|
+
const nextExpiry = (createdAt, now) => new Date(Math.min(now + idleMs, createdAt + ttlMs)).toISOString();
|
|
45
|
+
const unwrap = (cookieValue) => {
|
|
46
|
+
if (!cookieValue)
|
|
47
|
+
return null;
|
|
48
|
+
const separator = cookieValue.lastIndexOf(".");
|
|
49
|
+
if (separator <= 0)
|
|
50
|
+
return null;
|
|
51
|
+
const token = cookieValue.slice(0, separator);
|
|
52
|
+
const signature = cookieValue.slice(separator + 1);
|
|
53
|
+
if (!safeEqual(signature, sign(token, config.sessionSecret)))
|
|
54
|
+
return null;
|
|
55
|
+
return token;
|
|
56
|
+
};
|
|
57
|
+
return {
|
|
58
|
+
create({ identity, ip, userAgent }) {
|
|
59
|
+
const token = randomToken(32);
|
|
60
|
+
const csrfToken = randomToken(32);
|
|
61
|
+
const now = Date.now();
|
|
62
|
+
const record = sessions.create({
|
|
63
|
+
id: sha256(token),
|
|
64
|
+
identity,
|
|
65
|
+
csrfToken,
|
|
66
|
+
expiresAt: nextExpiry(now, now),
|
|
67
|
+
ip: ip ?? null,
|
|
68
|
+
userAgent: userAgent ?? null,
|
|
69
|
+
});
|
|
70
|
+
return { record, cookie: `${token}.${sign(token, config.sessionSecret)}`, csrfToken };
|
|
71
|
+
},
|
|
72
|
+
resolve(cookieValue) {
|
|
73
|
+
const token = unwrap(cookieValue);
|
|
74
|
+
if (!token)
|
|
75
|
+
return null;
|
|
76
|
+
const record = sessions.find(sha256(token));
|
|
77
|
+
if (!record)
|
|
78
|
+
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
|
+
return record;
|
|
90
|
+
},
|
|
91
|
+
destroy(cookieValue) {
|
|
92
|
+
const token = unwrap(cookieValue);
|
|
93
|
+
if (token)
|
|
94
|
+
sessions.delete(sha256(token));
|
|
95
|
+
},
|
|
96
|
+
destroyAllForIdentity(issuer, subject) {
|
|
97
|
+
sessions.deleteForIdentity(issuer, subject);
|
|
98
|
+
},
|
|
99
|
+
sessionCookieOptions() {
|
|
100
|
+
return {
|
|
101
|
+
path: "/",
|
|
102
|
+
httpOnly: true,
|
|
103
|
+
secure: config.cookieSecure,
|
|
104
|
+
// Lax keeps the cookie on top-level navigations back to the admin
|
|
105
|
+
// while blocking it on cross-site form posts.
|
|
106
|
+
sameSite: "Lax",
|
|
107
|
+
maxAge: Math.floor(ttlMs / 1000),
|
|
108
|
+
};
|
|
109
|
+
},
|
|
110
|
+
csrfCookieOptions() {
|
|
111
|
+
return {
|
|
112
|
+
path: "/",
|
|
113
|
+
// Readable by the SPA on purpose: it echoes the value in a header,
|
|
114
|
+
// which is what makes the double-submit check work.
|
|
115
|
+
httpOnly: false,
|
|
116
|
+
secure: config.cookieSecure,
|
|
117
|
+
sameSite: "Lax",
|
|
118
|
+
maxAge: Math.floor(ttlMs / 1000),
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
clearedCookieOptions() {
|
|
122
|
+
return {
|
|
123
|
+
path: "/",
|
|
124
|
+
httpOnly: true,
|
|
125
|
+
secure: config.cookieSecure,
|
|
126
|
+
sameSite: "Lax",
|
|
127
|
+
maxAge: 0,
|
|
128
|
+
};
|
|
129
|
+
},
|
|
130
|
+
purgeExpired() {
|
|
131
|
+
return sessions.deleteExpired();
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/* ------------------------------------------------------------------ *
|
|
136
|
+
* Starting one
|
|
137
|
+
* ------------------------------------------------------------------ */
|
|
138
|
+
/** Issues the session cookies for a browser that has just been identified. */
|
|
139
|
+
export function establishSession(c, sessions, identity) {
|
|
140
|
+
// Session fixation: discard whatever identifier the client already holds.
|
|
141
|
+
sessions.destroy(getCookie(c, SESSION_COOKIE));
|
|
142
|
+
const { cookie, csrfToken } = sessions.create({
|
|
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());
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=session.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,56 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,96 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sessions-repository.js","sourceRoot":"","sources":["../src/sessions-repository.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAiDH;;;;GAIG;AACH,SAAS,WAAW,CAAC,KAAa;IAChC,IAAI,CAAC;QACH,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC1C,OAAO,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAC1F,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,SAAS,QAAQ,CAAC,GAAe;IAC/B,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,QAAQ,EACN,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,OAAO;YACvB,CAAC,CAAC;gBACE,MAAM,EAAE,GAAG,CAAC,MAAM;gBAClB,OAAO,EAAE,GAAG,CAAC,OAAO;gBACpB,KAAK,EAAE,GAAG,CAAC,KAAK,IAAI,EAAE;gBACtB,aAAa,EAAE,GAAG,CAAC,cAAc,KAAK,CAAC;gBACvC,IAAI,EAAE,GAAG,CAAC,IAAI,IAAI,EAAE;gBACpB,MAAM,EAAE,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC;aAChC;YACH,CAAC,CAAC,IAAI;QACV,SAAS,EAAE,GAAG,CAAC,UAAU;QACzB,SAAS,EAAE,GAAG,CAAC,UAAU;QACzB,UAAU,EAAE,GAAG,CAAC,YAAY;QAC5B,SAAS,EAAE,GAAG,CAAC,UAAU;QACzB,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,SAAS,EAAE,GAAG,CAAC,UAAU;KAC1B,CAAC;AACJ,CAAC;AAED,MAAM,OAAO,GACX,wEAAwE;IACxE,sDAAsD,CAAC;AAEzD,MAAM,UAAU,uBAAuB,CAAC,EAAe;IACrD,OAAO;QACL,MAAM,CAAC,KAON;YACC,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;YACrC,EAAE,CAAC,GAAG,CACJ;cACM,OAAO,kDAAkD,EAC/D;gBACE,KAAK,CAAC,EAAE;gBACR,KAAK,CAAC,QAAQ,EAAE,MAAM,IAAI,IAAI;gBAC9B,KAAK,CAAC,QAAQ,EAAE,OAAO,IAAI,IAAI;gBAC/B,KAAK,CAAC,QAAQ,EAAE,KAAK,IAAI,IAAI;gBAC7B,KAAK,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;gBACrC,KAAK,CAAC,QAAQ,EAAE,IAAI,IAAI,IAAI;gBAC5B,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,QAAQ,EAAE,MAAM,IAAI,EAAE,CAAC;gBAC5C,KAAK,CAAC,SAAS;gBACf,GAAG;gBACH,GAAG;gBACH,KAAK,CAAC,SAAS;gBACf,KAAK,CAAC,EAAE,IAAI,IAAI;gBAChB,KAAK,CAAC,SAAS,IAAI,IAAI;aACxB,CACF,CAAC;YACF,OAAO,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAE,CAAC;QAC9B,CAAC;QAED,IAAI,CAAC,EAAU;YACb,MAAM,GAAG,GAAG,EAAE,CAAC,GAAG,CAAa,UAAU,OAAO,6BAA6B,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;YACrF,OAAO,GAAG,IAAI,QAAQ,CAAC,GAAG,CAAC,CAAC;QAC9B,CAAC;QAED,KAAK,CAAC,EAAU,EAAE,UAAkB,EAAE,SAAiB;YACrD,EAAE,CAAC,GAAG,CAAC,mEAAmE,EAAE;gBAC1E,UAAU;gBACV,SAAS;gBACT,EAAE;aACH,CAAC,CAAC;QACL,CAAC;QAED,MAAM,CAAC,EAAU;YACf,OAAO,EAAE,CAAC,GAAG,CAAC,mCAAmC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC;QACvE,CAAC;QAED,wDAAwD;QACxD,iBAAiB,CAAC,MAAc,EAAE,OAAe;YAC/C,OAAO,EAAE,CAAC,GAAG,CAAC,uDAAuD,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;iBACtF,OAAO,CAAC;QACb,CAAC;QAED,aAAa,CAAC,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YAC1C,OAAO,EAAE,CAAC,GAAG,CAAC,4CAA4C,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC;QAC7E,CAAC;QAED,KAAK;YACH,OAAO,EAAE,CAAC,GAAG,CAAoB,wCAAwC,CAAC,EAAE,KAAK,IAAI,CAAC,CAAC;QACzF,CAAC;KACF,CAAC;AACJ,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@micacms/auth-oidc",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
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
|
+
"license": "MIT",
|
|
7
|
+
"author": "Alexander Müdespacher",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"mica",
|
|
10
|
+
"cms",
|
|
11
|
+
"oidc",
|
|
12
|
+
"authentication"
|
|
13
|
+
],
|
|
14
|
+
"sideEffects": false,
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"default": "./dist/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist"
|
|
26
|
+
],
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"hono": "^4.6.20",
|
|
29
|
+
"openid-client": "^6.8.5",
|
|
30
|
+
"zod": "^4.3.6",
|
|
31
|
+
"@micacms/core": "0.2.0",
|
|
32
|
+
"@micacms/runtime": "0.2.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^22.20.1",
|
|
36
|
+
"tsx": "^4.19.2",
|
|
37
|
+
"typescript": "^5.7.3"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=22.12.0"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsc -p tsconfig.json",
|
|
44
|
+
"typecheck": "tsc -p tsconfig.test.json",
|
|
45
|
+
"test": "tsc -p tsconfig.json && node --test --import tsx test/*.test.ts"
|
|
46
|
+
}
|
|
47
|
+
}
|