@micacms/auth-oidc 0.6.0-rc.2 → 0.7.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/dist/gate.d.ts CHANGED
@@ -28,6 +28,8 @@ export interface GateDeps {
28
28
  * ones the provider sent, and Mica learns better at the next sign-in.
29
29
  */
30
30
  export declare function requireAccess(deps: GateDeps): MiddlewareHandler<Env>;
31
+ /** When the session ends, on every admitted response. */
32
+ export declare const SESSION_EXPIRES_HEADER = "x-mica-session-expires";
31
33
  /**
32
34
  * Second CSRF layer: double-submit token. The session's token is issued in a
33
35
  * cookie the SPA can read and must be echoed in a header.
@@ -1 +1 @@
1
- {"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../src/gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAEvD,OAAO,EAAY,KAAK,GAAG,EAAE,MAAM,kBAAkB,CAAC;AAEtD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACtD,OAAO,EAGL,KAAK,aAAa,EAClB,KAAK,cAAc,EACpB,MAAM,cAAc,CAAC;AAQtB,wBAAgB,SAAS,CAAC,CAAC,EAAE,OAAO,CAAC,GAAG,CAAC,GAAG,aAAa,CAGxD;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,EAAE,cAAc,CAAC;IACzB,SAAS,EAAE,eAAe,CAAC;CAC5B;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,QAAQ,GAAG,iBAAiB,CAAC,GAAG,CAAC,CAiCpE;AAID;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,QAAQ,GAAG,iBAAiB,CAAC,GAAG,CAAC,CAiBrE"}
1
+ {"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../src/gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAEvD,OAAO,EAAY,KAAK,GAAG,EAAE,MAAM,kBAAkB,CAAC;AAEtD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACtD,OAAO,EAGL,KAAK,aAAa,EAClB,KAAK,cAAc,EACpB,MAAM,cAAc,CAAC;AAQtB,wBAAgB,SAAS,CAAC,CAAC,EAAE,OAAO,CAAC,GAAG,CAAC,GAAG,aAAa,CAGxD;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,EAAE,cAAc,CAAC;IACzB,SAAS,EAAE,eAAe,CAAC;CAC5B;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,QAAQ,GAAG,iBAAiB,CAAC,GAAG,CAAC,CAmCpE;AAED,yDAAyD;AACzD,eAAO,MAAM,sBAAsB,2BAA2B,CAAC;AAI/D;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,QAAQ,GAAG,iBAAiB,CAAC,GAAG,CAAC,CAiBrE"}
package/dist/gate.js CHANGED
@@ -57,8 +57,12 @@ export function requireAccess(deps) {
57
57
  if (refreshed) {
58
58
  setCookie(c, deps.sessions.names.session, refreshed.cookie, deps.sessions.sessionCookieOptions());
59
59
  }
60
+ // The editor warns before the end from this, without asking (and so extending) the session.
61
+ c.header(SESSION_EXPIRES_HEADER, session.expiresAt);
60
62
  };
61
63
  }
64
+ /** When the session ends, on every admitted response. */
65
+ export const SESSION_EXPIRES_HEADER = "x-mica-session-expires";
62
66
  const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
63
67
  /**
64
68
  * Second CSRF layer: double-submit token. The session's token is issued in a
package/dist/gate.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"gate.js","sourceRoot":"","sources":["../src/gate.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The gate: what stands between a request and the admin API.\n *\n * Two layers, in order. `requireAccess` turns a session cookie into an\n * `AccessContext` — or a 401 — re-checking admission on every request so a\n * configuration change is immediate, and re-issuing the cookie while the\n * session is in use. `csrfProtection` is the double-submit\n * token over the Origin check the runtime already applies: cross-origin\n * JavaScript can neither read the cookie nor set the header.\n */\n\nimport { getCookie, setCookie } from \"hono/cookie\";\nimport type { Context, MiddlewareHandler } from \"hono\";\n\nimport { ApiError, type Env } from \"@micacms/runtime\";\n\nimport type { AdmissionPolicy } from \"./admission.js\";\nimport {\n CSRF_HEADER,\n safeEqual,\n type SessionRecord,\n type SessionService,\n} from \"./session.js\";\n\n/**\n * The session behind the current request. Auth's own context variable —\n * the runtime knows only the `access` it derives from it.\n */\nconst SESSION_VARIABLE = \"authSession\";\n\nexport function sessionOf(c: Context<Env>): SessionRecord {\n // Set by requireAccess, which runs in front of everything that calls this.\n return c.get(SESSION_VARIABLE as never) as SessionRecord;\n}\n\nexport interface GateDeps {\n sessions: SessionService;\n admission: AdmissionPolicy;\n}\n\n/**\n * Rejects the request unless there is a live session whose person is still\n * admitted.\n *\n * Admission is re-read on every request, not decided once at sign-in. That is\n * what makes a configuration change immediate: an operator who takes somebody\n * out of `MICA_ADMIT_SUBJECTS` and restarts has removed them. What it cannot\n * make immediate is a group removal at the provider — the claims here are the\n * ones the provider sent, and Mica learns better at the next sign-in.\n */\nexport function requireAccess(deps: GateDeps): MiddlewareHandler<Env> {\n return async (c, next) => {\n let session = deps.sessions.resolve(getCookie(c, deps.sessions.names.session));\n\n if (!session) {\n throw new ApiError(\"unauthenticated\", \"Your session has expired. Please sign in again.\");\n }\n\n const { identity } = session;\n\n const verdict = deps.admission.admits(identity);\n if (!verdict.admitted) {\n c.get(\"logger\").warn(\"Session no longer admitted\", {\n subject: identity.subject,\n reason: verdict.reason,\n });\n throw new ApiError(\"forbidden\", \"You no longer have access to this site.\");\n }\n\n const refreshed = deps.sessions.refresh(session);\n if (refreshed) session = refreshed.record;\n\n c.set(SESSION_VARIABLE as never, session as never);\n c.set(\"access\", {\n identity: { subject: identity.subject, email: identity.email, name: identity.name },\n });\n await next();\n\n // After the handler, so the cookie lands on whatever response it built.\n if (refreshed) {\n setCookie(c, deps.sessions.names.session, refreshed.cookie, deps.sessions.sessionCookieOptions());\n }\n };\n}\n\nconst SAFE_METHODS = new Set([\"GET\", \"HEAD\", \"OPTIONS\"]);\n\n/**\n * Second CSRF layer: double-submit token. The session's token is issued in a\n * cookie the SPA can read and must be echoed in a header.\n *\n * Runs after `requireAccess`, and complements the session cookie's\n * `SameSite=Lax`, which already blocks the classic cross-site form post.\n */\nexport function csrfProtection(deps: GateDeps): MiddlewareHandler<Env> {\n return async (c, next) => {\n if (SAFE_METHODS.has(c.req.method)) {\n await next();\n return;\n }\n\n const session = sessionOf(c);\n const header = c.req.header(CSRF_HEADER);\n const cookie = getCookie(c, deps.sessions.names.csrf);\n\n if (!header || !cookie || !safeEqual(header, cookie) || !safeEqual(header, session.csrfToken)) {\n throw new ApiError(\"forbidden\", \"Invalid or missing CSRF token. Reload the page and retry.\");\n }\n\n await next();\n };\n}\n"]}
1
+ {"version":3,"file":"gate.js","sourceRoot":"","sources":["../src/gate.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The gate: what stands between a request and the admin API.\n *\n * Two layers, in order. `requireAccess` turns a session cookie into an\n * `AccessContext` — or a 401 — re-checking admission on every request so a\n * configuration change is immediate, and re-issuing the cookie while the\n * session is in use. `csrfProtection` is the double-submit\n * token over the Origin check the runtime already applies: cross-origin\n * JavaScript can neither read the cookie nor set the header.\n */\n\nimport { getCookie, setCookie } from \"hono/cookie\";\nimport type { Context, MiddlewareHandler } from \"hono\";\n\nimport { ApiError, type Env } from \"@micacms/runtime\";\n\nimport type { AdmissionPolicy } from \"./admission.js\";\nimport {\n CSRF_HEADER,\n safeEqual,\n type SessionRecord,\n type SessionService,\n} from \"./session.js\";\n\n/**\n * The session behind the current request. Auth's own context variable —\n * the runtime knows only the `access` it derives from it.\n */\nconst SESSION_VARIABLE = \"authSession\";\n\nexport function sessionOf(c: Context<Env>): SessionRecord {\n // Set by requireAccess, which runs in front of everything that calls this.\n return c.get(SESSION_VARIABLE as never) as SessionRecord;\n}\n\nexport interface GateDeps {\n sessions: SessionService;\n admission: AdmissionPolicy;\n}\n\n/**\n * Rejects the request unless there is a live session whose person is still\n * admitted.\n *\n * Admission is re-read on every request, not decided once at sign-in. That is\n * what makes a configuration change immediate: an operator who takes somebody\n * out of `MICA_ADMIT_SUBJECTS` and restarts has removed them. What it cannot\n * make immediate is a group removal at the provider — the claims here are the\n * ones the provider sent, and Mica learns better at the next sign-in.\n */\nexport function requireAccess(deps: GateDeps): MiddlewareHandler<Env> {\n return async (c, next) => {\n let session = deps.sessions.resolve(getCookie(c, deps.sessions.names.session));\n\n if (!session) {\n throw new ApiError(\"unauthenticated\", \"Your session has expired. Please sign in again.\");\n }\n\n const { identity } = session;\n\n const verdict = deps.admission.admits(identity);\n if (!verdict.admitted) {\n c.get(\"logger\").warn(\"Session no longer admitted\", {\n subject: identity.subject,\n reason: verdict.reason,\n });\n throw new ApiError(\"forbidden\", \"You no longer have access to this site.\");\n }\n\n const refreshed = deps.sessions.refresh(session);\n if (refreshed) session = refreshed.record;\n\n c.set(SESSION_VARIABLE as never, session as never);\n c.set(\"access\", {\n identity: { subject: identity.subject, email: identity.email, name: identity.name },\n });\n await next();\n\n // After the handler, so the cookie lands on whatever response it built.\n if (refreshed) {\n setCookie(c, deps.sessions.names.session, refreshed.cookie, deps.sessions.sessionCookieOptions());\n }\n // The editor warns before the end from this, without asking (and so extending) the session.\n c.header(SESSION_EXPIRES_HEADER, session.expiresAt);\n };\n}\n\n/** When the session ends, on every admitted response. */\nexport const SESSION_EXPIRES_HEADER = \"x-mica-session-expires\";\n\nconst SAFE_METHODS = new Set([\"GET\", \"HEAD\", \"OPTIONS\"]);\n\n/**\n * Second CSRF layer: double-submit token. The session's token is issued in a\n * cookie the SPA can read and must be echoed in a header.\n *\n * Runs after `requireAccess`, and complements the session cookie's\n * `SameSite=Lax`, which already blocks the classic cross-site form post.\n */\nexport function csrfProtection(deps: GateDeps): MiddlewareHandler<Env> {\n return async (c, next) => {\n if (SAFE_METHODS.has(c.req.method)) {\n await next();\n return;\n }\n\n const session = sessionOf(c);\n const header = c.req.header(CSRF_HEADER);\n const cookie = getCookie(c, deps.sessions.names.csrf);\n\n if (!header || !cookie || !safeEqual(header, cookie) || !safeEqual(header, session.csrfToken)) {\n throw new ApiError(\"forbidden\", \"Invalid or missing CSRF token. Reload the page and retry.\");\n }\n\n await next();\n };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@micacms/auth-oidc",
3
- "version": "0.6.0-rc.2",
3
+ "version": "0.7.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",
@@ -36,8 +36,8 @@
36
36
  "hono": "^4.13.7",
37
37
  "openid-client": "^6.8.5",
38
38
  "zod": "^4.3.6",
39
- "@micacms/runtime": "0.6.0-rc.2",
40
- "@micacms/core": "0.6.0-rc.2"
39
+ "@micacms/core": "0.7.0",
40
+ "@micacms/runtime": "0.7.0"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "^22.20.1",
package/src/gate.ts CHANGED
@@ -80,9 +80,14 @@ export function requireAccess(deps: GateDeps): MiddlewareHandler<Env> {
80
80
  if (refreshed) {
81
81
  setCookie(c, deps.sessions.names.session, refreshed.cookie, deps.sessions.sessionCookieOptions());
82
82
  }
83
+ // The editor warns before the end from this, without asking (and so extending) the session.
84
+ c.header(SESSION_EXPIRES_HEADER, session.expiresAt);
83
85
  };
84
86
  }
85
87
 
88
+ /** When the session ends, on every admitted response. */
89
+ export const SESSION_EXPIRES_HEADER = "x-mica-session-expires";
90
+
86
91
  const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
87
92
 
88
93
  /**