@monotykamary/localterm-server 2.33.0 → 2.35.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/caffeinate-battery.d.ts +5 -0
- package/dist/caffeinate-battery.d.ts.map +1 -1
- package/dist/caffeinate-battery.js +69 -1
- package/dist/caffeinate-battery.js.map +1 -1
- package/dist/caffeinate-controller.d.ts.map +1 -1
- package/dist/caffeinate-controller.js +3 -18
- package/dist/caffeinate-controller.js.map +1 -1
- package/dist/caffeinate-manager.js +5 -5
- package/dist/caffeinate-manager.js.map +1 -1
- package/dist/caffeinate-platform.d.ts +10 -0
- package/dist/caffeinate-platform.d.ts.map +1 -0
- package/dist/caffeinate-platform.js +73 -0
- package/dist/caffeinate-platform.js.map +1 -0
- package/dist/cdp/cdp-client.d.ts +30 -0
- package/dist/cdp/cdp-client.d.ts.map +1 -1
- package/dist/cdp/cdp-client.js +80 -0
- package/dist/cdp/cdp-client.js.map +1 -1
- package/dist/constants.d.ts +19 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +57 -8
- package/dist/constants.js.map +1 -1
- package/dist/daemon-config-store.d.ts +2 -0
- package/dist/daemon-config-store.d.ts.map +1 -1
- package/dist/daemon-config-store.js +14 -1
- package/dist/daemon-config-store.js.map +1 -1
- package/dist/identity/credential-store.d.ts +18 -0
- package/dist/identity/credential-store.d.ts.map +1 -0
- package/dist/identity/credential-store.js +76 -0
- package/dist/identity/credential-store.js.map +1 -0
- package/dist/identity/factory.d.ts +3 -0
- package/dist/identity/factory.d.ts.map +1 -0
- package/dist/identity/factory.js +19 -0
- package/dist/identity/factory.js.map +1 -0
- package/dist/identity/header-provider.d.ts +3 -0
- package/dist/identity/header-provider.d.ts.map +1 -0
- package/dist/identity/header-provider.js +33 -0
- package/dist/identity/header-provider.js.map +1 -0
- package/dist/identity/oidc-provider.d.ts +4 -0
- package/dist/identity/oidc-provider.d.ts.map +1 -0
- package/dist/identity/oidc-provider.js +172 -0
- package/dist/identity/oidc-provider.js.map +1 -0
- package/dist/identity/passkey-provider.d.ts +3 -0
- package/dist/identity/passkey-provider.d.ts.map +1 -0
- package/dist/identity/passkey-provider.js +233 -0
- package/dist/identity/passkey-provider.js.map +1 -0
- package/dist/identity/proxy-allowlist.d.ts +5 -0
- package/dist/identity/proxy-allowlist.d.ts.map +1 -0
- package/dist/identity/proxy-allowlist.js +64 -0
- package/dist/identity/proxy-allowlist.js.map +1 -0
- package/dist/identity/resolve.d.ts +11 -0
- package/dist/identity/resolve.d.ts.map +1 -0
- package/dist/identity/resolve.js +57 -0
- package/dist/identity/resolve.js.map +1 -0
- package/dist/identity/session-cookie.d.ts +10 -0
- package/dist/identity/session-cookie.d.ts.map +1 -0
- package/dist/identity/session-cookie.js +92 -0
- package/dist/identity/session-cookie.js.map +1 -0
- package/dist/identity/types.d.ts +49 -0
- package/dist/identity/types.d.ts.map +1 -0
- package/dist/identity/types.js +2 -0
- package/dist/identity/types.js.map +1 -0
- package/dist/identity/user-store.d.ts +16 -0
- package/dist/identity/user-store.d.ts.map +1 -0
- package/dist/identity/user-store.js +77 -0
- package/dist/identity/user-store.js.map +1 -0
- package/dist/index.d.ts +16 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +112 -31
- package/dist/index.js.map +1 -1
- package/dist/protocol.d.ts +2 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +1 -1
- package/dist/protocol.js.map +1 -1
- package/dist/schemas.d.ts +79 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +58 -3
- package/dist/schemas.js.map +1 -1
- package/dist/secret-store.d.ts.map +1 -1
- package/dist/secret-store.js +4 -1
- package/dist/secret-store.js.map +1 -1
- package/dist/session-automation.d.ts +7 -2
- package/dist/session-automation.d.ts.map +1 -1
- package/dist/session-automation.js +27 -8
- package/dist/session-automation.js.map +1 -1
- package/dist/session-manager.d.ts +20 -17
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/session-manager.js +63 -44
- package/dist/session-manager.js.map +1 -1
- package/dist/utils/find-binary-on-path.d.ts +2 -0
- package/dist/utils/find-binary-on-path.d.ts.map +1 -0
- package/dist/utils/find-binary-on-path.js +24 -0
- package/dist/utils/find-binary-on-path.js.map +1 -0
- package/dist/utils/open-chrome-inspect.d.ts.map +1 -1
- package/dist/utils/open-chrome-inspect.js +55 -5
- package/dist/utils/open-chrome-inspect.js.map +1 -1
- package/dist/utils/timing-safe-equal.d.ts +2 -0
- package/dist/utils/timing-safe-equal.d.ts.map +1 -0
- package/dist/utils/timing-safe-equal.js +12 -0
- package/dist/utils/timing-safe-equal.js.map +1 -0
- package/package.json +4 -1
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import * as oauth from "oauth4webapi";
|
|
3
|
+
import { AUTH_STATE_TTL_MS, IDENTITY_USER_MAX_LENGTH } from "../constants.js";
|
|
4
|
+
import { clearSessionCookie, readSessionIdentity, setSessionCookie } from "./session-cookie.js";
|
|
5
|
+
class OidcStateStore {
|
|
6
|
+
states = new Map();
|
|
7
|
+
set(state, entry) {
|
|
8
|
+
this.states.set(state, entry);
|
|
9
|
+
const now = Date.now();
|
|
10
|
+
for (const [key, value] of this.states) {
|
|
11
|
+
if (value.expiresAt < now)
|
|
12
|
+
this.states.delete(key);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
consume(state) {
|
|
16
|
+
const entry = this.states.get(state);
|
|
17
|
+
this.states.delete(state);
|
|
18
|
+
if (!entry || entry.expiresAt < Date.now())
|
|
19
|
+
return null;
|
|
20
|
+
return entry;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// Only allow same-origin relative paths as the post-login landing target, to
|
|
24
|
+
// keep the callback from being an open redirect: a value must start with `/`
|
|
25
|
+
// and not `//` (a protocol-relative URL the browser would treat as absolute).
|
|
26
|
+
export const sanitizeReturnTo = (value) => {
|
|
27
|
+
if (!value || !value.startsWith("/") || value.startsWith("//"))
|
|
28
|
+
return "/";
|
|
29
|
+
return value;
|
|
30
|
+
};
|
|
31
|
+
const buildRedirectUri = (origin) => `${origin.replace(/\/$/, "")}/auth/oidc/callback`;
|
|
32
|
+
const buildOidcRoutes = (deps) => {
|
|
33
|
+
const app = new Hono();
|
|
34
|
+
// GET /oidc/login?returnTo=<path> — kick off the auth-code flow: mint PKCE +
|
|
35
|
+
// state + nonce, remember them against `state`, 302 to the IdP's authz
|
|
36
|
+
// endpoint. The `redirect_uri` is the daemon's announced origin + this
|
|
37
|
+
// callback path, which must be registered with the IdP — so OIDC needs a
|
|
38
|
+
// stable announced origin (the tailnet/local-https surface), unlike passkey
|
|
39
|
+
// which binds to whatever origin the browser is on.
|
|
40
|
+
app.get("/oidc/login", async (context) => {
|
|
41
|
+
const origin = deps.getOrigin();
|
|
42
|
+
if (!origin)
|
|
43
|
+
return context.json({ error: "no_origin" }, 500);
|
|
44
|
+
let metadata;
|
|
45
|
+
try {
|
|
46
|
+
metadata = await deps.getMetadata();
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return context.json({ error: "issuer_unreachable" }, 502);
|
|
50
|
+
}
|
|
51
|
+
if (!metadata.authorization_endpoint) {
|
|
52
|
+
return context.json({ error: "issuer_unsupported" }, 502);
|
|
53
|
+
}
|
|
54
|
+
const redirectUri = buildRedirectUri(origin);
|
|
55
|
+
const codeVerifier = oauth.generateRandomCodeVerifier();
|
|
56
|
+
const codeChallenge = await oauth.calculatePKCECodeChallenge(codeVerifier);
|
|
57
|
+
const state = oauth.generateRandomState();
|
|
58
|
+
const nonce = oauth.generateRandomNonce();
|
|
59
|
+
const returnTo = sanitizeReturnTo(new URL(context.req.url).searchParams.get("returnTo"));
|
|
60
|
+
deps.stateStore.set(state, {
|
|
61
|
+
codeVerifier,
|
|
62
|
+
nonce,
|
|
63
|
+
returnTo,
|
|
64
|
+
expiresAt: Date.now() + AUTH_STATE_TTL_MS,
|
|
65
|
+
});
|
|
66
|
+
const params = new URLSearchParams();
|
|
67
|
+
params.set("client_id", deps.client.client_id);
|
|
68
|
+
params.set("redirect_uri", redirectUri);
|
|
69
|
+
params.set("response_type", "code");
|
|
70
|
+
params.set("scope", deps.scope);
|
|
71
|
+
params.set("state", state);
|
|
72
|
+
params.set("nonce", nonce);
|
|
73
|
+
params.set("code_challenge", codeChallenge);
|
|
74
|
+
params.set("code_challenge_method", "S256");
|
|
75
|
+
const authUrl = new URL(metadata.authorization_endpoint);
|
|
76
|
+
for (const [key, value] of params.entries()) {
|
|
77
|
+
authUrl.searchParams.set(key, value);
|
|
78
|
+
}
|
|
79
|
+
return context.redirect(authUrl.toString(), 302);
|
|
80
|
+
});
|
|
81
|
+
// GET /oidc/callback?code=&state= — the IdP redirects here. Consume the
|
|
82
|
+
// state, validate the response, exchange the code for tokens (verifying the
|
|
83
|
+
// ID-token nonce), fetch userinfo, and issue a session cookie for the
|
|
84
|
+
// configured claim (default email, falling back to `sub`). Any failure
|
|
85
|
+
// redirects to `/` rather than leaking an error page to the browser.
|
|
86
|
+
app.get("/oidc/callback", async (context) => {
|
|
87
|
+
const origin = deps.getOrigin();
|
|
88
|
+
if (!origin)
|
|
89
|
+
return context.redirect("/", 302);
|
|
90
|
+
const callbackUrl = new URL(context.req.url);
|
|
91
|
+
const state = callbackUrl.searchParams.get("state");
|
|
92
|
+
if (!state)
|
|
93
|
+
return context.redirect("/", 302);
|
|
94
|
+
const stored = deps.stateStore.consume(state);
|
|
95
|
+
if (!stored)
|
|
96
|
+
return context.redirect("/", 302);
|
|
97
|
+
try {
|
|
98
|
+
const metadata = await deps.getMetadata();
|
|
99
|
+
const validated = oauth.validateAuthResponse(metadata, deps.client, callbackUrl.searchParams, state);
|
|
100
|
+
const tokenResponse = await oauth.authorizationCodeGrantRequest(metadata, deps.client, deps.clientAuth, validated, buildRedirectUri(origin), stored.codeVerifier);
|
|
101
|
+
const tokens = await oauth.processAuthorizationCodeResponse(metadata, deps.client, tokenResponse, {
|
|
102
|
+
expectedNonce: stored.nonce,
|
|
103
|
+
});
|
|
104
|
+
const userInfo = await oauth.processUserInfoResponse(metadata, deps.client, oauth.skipSubjectCheck, await oauth.userInfoRequest(metadata, deps.client, tokens.access_token));
|
|
105
|
+
const raw = userInfo[deps.claim];
|
|
106
|
+
const user = (typeof raw === "string" ? raw : userInfo.sub).slice(0, IDENTITY_USER_MAX_LENGTH);
|
|
107
|
+
setSessionCookie(context, deps.secret, user);
|
|
108
|
+
return context.redirect(stored.returnTo, 302);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return context.redirect("/", 302);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
app.post("/oidc/logout", (context) => {
|
|
115
|
+
clearSessionCookie(context);
|
|
116
|
+
return context.json({ ok: true });
|
|
117
|
+
});
|
|
118
|
+
app.get("/oidc/me", (context) => {
|
|
119
|
+
const identity = readSessionIdentity(context, deps.secret);
|
|
120
|
+
return context.json({ user: identity?.user ?? null });
|
|
121
|
+
});
|
|
122
|
+
return app;
|
|
123
|
+
};
|
|
124
|
+
// The bring-your-own-IdP provider: any OIDC IdP (Google, GitHub, or self-hosted
|
|
125
|
+
// Authentik/Zitadel/Keycloak) authenticates via an authorization-code + PKCE
|
|
126
|
+
// flow; localterm keeps no passwords. Like `passkey`, `identify` reads the
|
|
127
|
+
// signed session cookie the callback issued and `denyUnauthenticated` is true
|
|
128
|
+
// (the gate rejects a no-session request). Discovery is cached lazily and
|
|
129
|
+
// retried on failure; the `redirect_uri` is the daemon's announced origin.
|
|
130
|
+
export const createOidcIdentityProvider = (config, deps) => {
|
|
131
|
+
const issuerUrl = new URL(config.issuer);
|
|
132
|
+
const client = { client_id: config.clientId };
|
|
133
|
+
const clientAuth = config.clientSecret
|
|
134
|
+
? oauth.ClientSecretPost(config.clientSecret)
|
|
135
|
+
: oauth.None();
|
|
136
|
+
const claim = config.claim ?? "email";
|
|
137
|
+
const scope = config.scope ?? "openid email";
|
|
138
|
+
const stateStore = new OidcStateStore();
|
|
139
|
+
const secret = deps.secret;
|
|
140
|
+
// Cached OIDC discovery (the IdP's metadata). Resolved once, shared across
|
|
141
|
+
// flows; reset to null on failure so the next attempt re-discovers rather
|
|
142
|
+
// than caching a bad result. A single shared promise avoids duplicate
|
|
143
|
+
// concurrent discoveries.
|
|
144
|
+
let metadataPromise = null;
|
|
145
|
+
const getMetadata = () => {
|
|
146
|
+
if (!metadataPromise) {
|
|
147
|
+
metadataPromise = (async () => oauth.processDiscoveryResponse(issuerUrl, await oauth.discoveryRequest(issuerUrl)))();
|
|
148
|
+
metadataPromise.catch(() => {
|
|
149
|
+
metadataPromise = null;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return metadataPromise;
|
|
153
|
+
};
|
|
154
|
+
return {
|
|
155
|
+
kind: "oidc",
|
|
156
|
+
denyUnauthenticated: true,
|
|
157
|
+
operatorToken: config.operatorToken ?? null,
|
|
158
|
+
identify: (context) => readSessionIdentity(context, secret),
|
|
159
|
+
routes: () => buildOidcRoutes({
|
|
160
|
+
issuerUrl,
|
|
161
|
+
client,
|
|
162
|
+
clientAuth,
|
|
163
|
+
claim,
|
|
164
|
+
scope,
|
|
165
|
+
getOrigin: deps.getOrigin,
|
|
166
|
+
stateStore,
|
|
167
|
+
secret,
|
|
168
|
+
getMetadata,
|
|
169
|
+
}),
|
|
170
|
+
};
|
|
171
|
+
};
|
|
172
|
+
//# sourceMappingURL=oidc-provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oidc-provider.js","sourceRoot":"","sources":["../../src/identity/oidc-provider.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { IdentityProvider, IdentityProviderDeps, PasskeyIdentityConfig } from "./types.js";
|
|
2
|
+
export declare const createPasskeyIdentityProvider: (config: PasskeyIdentityConfig, deps: IdentityProviderDeps) => IdentityProvider;
|
|
3
|
+
//# sourceMappingURL=passkey-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"passkey-provider.d.ts","sourceRoot":"","sources":["../../src/identity/passkey-provider.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAEV,gBAAgB,EAChB,oBAAoB,EACpB,qBAAqB,EACtB,MAAM,YAAY,CAAC;AA0OpB,eAAO,MAAM,6BAA6B,GACxC,QAAQ,qBAAqB,EAC7B,MAAM,oBAAoB,KACzB,gBAwBF,CAAC"}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { generateAuthenticationOptions, generateRegistrationOptions, verifyAuthenticationResponse, verifyRegistrationResponse, } from "@simplewebauthn/server";
|
|
4
|
+
import { AUTH_CHALLENGE_TTL_MS, HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_FORBIDDEN, IDENTITY_RP_NAME_DEFAULT, IDENTITY_USERNAME_MAX_LENGTH, IDENTITY_USERNAME_MIN_LENGTH, } from "../constants.js";
|
|
5
|
+
import { CredentialStore } from "./credential-store.js";
|
|
6
|
+
import { UserStore } from "./user-store.js";
|
|
7
|
+
import { clearSessionCookie, readSessionIdentity, setSessionCookie } from "./session-cookie.js";
|
|
8
|
+
class ChallengeStore {
|
|
9
|
+
challenges = new Map();
|
|
10
|
+
set(challenge, kind) {
|
|
11
|
+
this.challenges.set(challenge, { kind, expiresAt: Date.now() + AUTH_CHALLENGE_TTL_MS });
|
|
12
|
+
const now = Date.now();
|
|
13
|
+
for (const [key, entry] of this.challenges) {
|
|
14
|
+
if (entry.expiresAt < now)
|
|
15
|
+
this.challenges.delete(key);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
// Single-use: delete on read, return true only if it matched the expected
|
|
19
|
+
// kind and hadn't expired. A register challenge can't satisfy a login verify
|
|
20
|
+
// (and vice versa), and a consumed challenge can't be replayed.
|
|
21
|
+
consume(challenge, kind) {
|
|
22
|
+
const entry = this.challenges.get(challenge);
|
|
23
|
+
this.challenges.delete(challenge);
|
|
24
|
+
return entry?.kind === kind && entry.expiresAt >= Date.now();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
28
|
+
const readBody = async (context) => {
|
|
29
|
+
try {
|
|
30
|
+
const json = await context.req.json();
|
|
31
|
+
return isObject(json) ? json : {};
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const normalizeUsername = (value) => {
|
|
38
|
+
if (typeof value !== "string")
|
|
39
|
+
return null;
|
|
40
|
+
const trimmed = value.trim();
|
|
41
|
+
if (trimmed.length < IDENTITY_USERNAME_MIN_LENGTH ||
|
|
42
|
+
trimmed.length > IDENTITY_USERNAME_MAX_LENGTH) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return trimmed;
|
|
46
|
+
};
|
|
47
|
+
// The RP origin/id come from the browser's own Origin header (the surface the
|
|
48
|
+
// user is actually on), falling back to the daemon's announced origin. A
|
|
49
|
+
// passkey is bound to the RP ID (hostname), so this is also why a passkey
|
|
50
|
+
// registered on the loopback origin won't work on the tailnet origin and
|
|
51
|
+
// vice-versa — inherent to WebAuthn, surfaced here as expectedOrigin/RPID.
|
|
52
|
+
const resolveRp = (context, getOrigin) => {
|
|
53
|
+
const raw = context.req.header("origin") || getOrigin();
|
|
54
|
+
if (!raw)
|
|
55
|
+
return null;
|
|
56
|
+
try {
|
|
57
|
+
const url = new URL(raw);
|
|
58
|
+
if (!url.hostname)
|
|
59
|
+
return null;
|
|
60
|
+
return { origin: url.origin, rpID: url.hostname };
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
// Quick structural reject for the credential response the browser sends; the
|
|
67
|
+
// real validation is simplewebauthn's verify (which throws on malformed input,
|
|
68
|
+
// caught by the route). The predicate narrows to the library's exact type so
|
|
69
|
+
// no cast is needed at the verify call.
|
|
70
|
+
const isRegistrationResponse = (value) => isObject(value) && typeof value.id === "string" && isObject(value.response);
|
|
71
|
+
const isAuthenticationResponse = (value) => isObject(value) && typeof value.id === "string" && isObject(value.response);
|
|
72
|
+
const buildPasskeyRoutes = (deps) => {
|
|
73
|
+
const app = new Hono();
|
|
74
|
+
app.get("/passkey/me", (context) => {
|
|
75
|
+
const identity = readSessionIdentity(context, deps.secret);
|
|
76
|
+
return context.json({ user: identity?.user ?? null });
|
|
77
|
+
});
|
|
78
|
+
app.post("/passkey/register/options", async (context) => {
|
|
79
|
+
if (!deps.registrationOpen) {
|
|
80
|
+
return context.json({ error: "registration_closed" }, HTTP_STATUS_FORBIDDEN);
|
|
81
|
+
}
|
|
82
|
+
const body = await readBody(context);
|
|
83
|
+
const username = normalizeUsername(body.username);
|
|
84
|
+
if (!username)
|
|
85
|
+
return context.json({ error: "invalid_username" }, HTTP_STATUS_BAD_REQUEST);
|
|
86
|
+
const rp = resolveRp(context, deps.getOrigin);
|
|
87
|
+
if (!rp)
|
|
88
|
+
return context.json({ error: "invalid_origin" }, HTTP_STATUS_BAD_REQUEST);
|
|
89
|
+
const excludeCredentials = (deps.userStore.get(username)?.credentialIds ?? []).map((id) => ({
|
|
90
|
+
id,
|
|
91
|
+
}));
|
|
92
|
+
const options = await generateRegistrationOptions({
|
|
93
|
+
rpName: deps.rpName,
|
|
94
|
+
rpID: rp.rpID,
|
|
95
|
+
userName: username,
|
|
96
|
+
excludeCredentials,
|
|
97
|
+
authenticatorSelection: { residentKey: "preferred", userVerification: "preferred" },
|
|
98
|
+
});
|
|
99
|
+
deps.challenges.set(options.challenge, "register");
|
|
100
|
+
return context.json(options);
|
|
101
|
+
});
|
|
102
|
+
app.post("/passkey/register/verify", async (context) => {
|
|
103
|
+
if (!deps.registrationOpen) {
|
|
104
|
+
return context.json({ error: "registration_closed" }, HTTP_STATUS_FORBIDDEN);
|
|
105
|
+
}
|
|
106
|
+
const body = await readBody(context);
|
|
107
|
+
const username = normalizeUsername(body.username);
|
|
108
|
+
if (!username || !isRegistrationResponse(body.response)) {
|
|
109
|
+
return context.json({ error: "invalid_body" }, HTTP_STATUS_BAD_REQUEST);
|
|
110
|
+
}
|
|
111
|
+
const rp = resolveRp(context, deps.getOrigin);
|
|
112
|
+
if (!rp)
|
|
113
|
+
return context.json({ error: "invalid_origin" }, HTTP_STATUS_BAD_REQUEST);
|
|
114
|
+
const response = body.response;
|
|
115
|
+
let verified;
|
|
116
|
+
try {
|
|
117
|
+
verified = await verifyRegistrationResponse({
|
|
118
|
+
response,
|
|
119
|
+
expectedChallenge: (challenge) => deps.challenges.consume(challenge, "register"),
|
|
120
|
+
expectedOrigin: rp.origin,
|
|
121
|
+
expectedRPID: rp.rpID,
|
|
122
|
+
requireUserVerification: true,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return context.json({ error: "verification_failed" }, HTTP_STATUS_BAD_REQUEST);
|
|
127
|
+
}
|
|
128
|
+
if (!verified || !verified.verified || !verified.registrationInfo) {
|
|
129
|
+
return context.json({ error: "verification_failed" }, HTTP_STATUS_BAD_REQUEST);
|
|
130
|
+
}
|
|
131
|
+
const credential = verified.registrationInfo.credential;
|
|
132
|
+
deps.userStore.findOrCreate(username);
|
|
133
|
+
deps.userStore.addCredential(username, credential.id);
|
|
134
|
+
deps.credentialStore.put({
|
|
135
|
+
id: credential.id,
|
|
136
|
+
publicKey: Buffer.from(credential.publicKey).toString("base64"),
|
|
137
|
+
counter: credential.counter,
|
|
138
|
+
username,
|
|
139
|
+
});
|
|
140
|
+
setSessionCookie(context, deps.secret, username);
|
|
141
|
+
return context.json({ user: username });
|
|
142
|
+
});
|
|
143
|
+
app.post("/passkey/login/options", async (context) => {
|
|
144
|
+
const body = await readBody(context);
|
|
145
|
+
const username = normalizeUsername(body.username);
|
|
146
|
+
const rp = resolveRp(context, deps.getOrigin);
|
|
147
|
+
if (!rp)
|
|
148
|
+
return context.json({ error: "invalid_origin" }, HTTP_STATUS_BAD_REQUEST);
|
|
149
|
+
const allowCredentials = username
|
|
150
|
+
? (deps.userStore.get(username)?.credentialIds ?? []).map((id) => ({ id }))
|
|
151
|
+
: undefined;
|
|
152
|
+
const options = await generateAuthenticationOptions({
|
|
153
|
+
rpID: rp.rpID,
|
|
154
|
+
allowCredentials,
|
|
155
|
+
userVerification: "preferred",
|
|
156
|
+
});
|
|
157
|
+
deps.challenges.set(options.challenge, "login");
|
|
158
|
+
return context.json(options);
|
|
159
|
+
});
|
|
160
|
+
app.post("/passkey/login/verify", async (context) => {
|
|
161
|
+
const body = await readBody(context);
|
|
162
|
+
if (!isAuthenticationResponse(body.response)) {
|
|
163
|
+
return context.json({ error: "invalid_body" }, HTTP_STATUS_BAD_REQUEST);
|
|
164
|
+
}
|
|
165
|
+
const response = body.response;
|
|
166
|
+
const rp = resolveRp(context, deps.getOrigin);
|
|
167
|
+
if (!rp)
|
|
168
|
+
return context.json({ error: "invalid_origin" }, HTTP_STATUS_BAD_REQUEST);
|
|
169
|
+
const stored = deps.credentialStore.get(response.id);
|
|
170
|
+
if (!stored)
|
|
171
|
+
return context.json({ error: "unknown_credential" }, HTTP_STATUS_BAD_REQUEST);
|
|
172
|
+
const credential = {
|
|
173
|
+
id: stored.id,
|
|
174
|
+
publicKey: Buffer.from(stored.publicKey, "base64"),
|
|
175
|
+
counter: stored.counter,
|
|
176
|
+
};
|
|
177
|
+
let verified;
|
|
178
|
+
try {
|
|
179
|
+
verified = await verifyAuthenticationResponse({
|
|
180
|
+
response,
|
|
181
|
+
expectedChallenge: (challenge) => deps.challenges.consume(challenge, "login"),
|
|
182
|
+
expectedOrigin: rp.origin,
|
|
183
|
+
expectedRPID: rp.rpID,
|
|
184
|
+
credential,
|
|
185
|
+
requireUserVerification: true,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return context.json({ error: "verification_failed" }, HTTP_STATUS_BAD_REQUEST);
|
|
190
|
+
}
|
|
191
|
+
if (!verified || !verified.verified) {
|
|
192
|
+
return context.json({ error: "verification_failed" }, HTTP_STATUS_BAD_REQUEST);
|
|
193
|
+
}
|
|
194
|
+
deps.credentialStore.updateCounter(stored.id, verified.authenticationInfo.newCounter);
|
|
195
|
+
setSessionCookie(context, deps.secret, stored.username);
|
|
196
|
+
return context.json({ user: stored.username });
|
|
197
|
+
});
|
|
198
|
+
app.post("/passkey/logout", (context) => {
|
|
199
|
+
clearSessionCookie(context);
|
|
200
|
+
return context.json({ ok: true });
|
|
201
|
+
});
|
|
202
|
+
return app;
|
|
203
|
+
};
|
|
204
|
+
// The self-contained identity provider: localterm is the identity authority.
|
|
205
|
+
// `identify` reads the signed session cookie the register/login flow set;
|
|
206
|
+
// `denyUnauthenticated: true` makes the gate reject any request without a
|
|
207
|
+
// valid session (401 / WS policy-violation) — unlike `header`, there's no
|
|
208
|
+
// operator fallback, because there's no external proxy to vouch for one.
|
|
209
|
+
// `routes()` is the `/auth/passkey/*` login flow mounted by the daemon.
|
|
210
|
+
export const createPasskeyIdentityProvider = (config, deps) => {
|
|
211
|
+
const rpName = config.rpName?.trim() || IDENTITY_RP_NAME_DEFAULT;
|
|
212
|
+
const registrationOpen = (config.registration ?? "open") === "open";
|
|
213
|
+
const userStore = new UserStore(path.join(deps.stateDirectory, "users.json"));
|
|
214
|
+
const credentialStore = new CredentialStore(path.join(deps.stateDirectory, "credentials.json"));
|
|
215
|
+
const challenges = new ChallengeStore();
|
|
216
|
+
const secret = deps.secret;
|
|
217
|
+
return {
|
|
218
|
+
kind: "passkey",
|
|
219
|
+
denyUnauthenticated: true,
|
|
220
|
+
operatorToken: config.operatorToken ?? null,
|
|
221
|
+
identify: (context) => readSessionIdentity(context, secret),
|
|
222
|
+
routes: () => buildPasskeyRoutes({
|
|
223
|
+
rpName,
|
|
224
|
+
registrationOpen,
|
|
225
|
+
getOrigin: deps.getOrigin,
|
|
226
|
+
userStore,
|
|
227
|
+
credentialStore,
|
|
228
|
+
challenges,
|
|
229
|
+
secret,
|
|
230
|
+
}),
|
|
231
|
+
};
|
|
232
|
+
};
|
|
233
|
+
//# sourceMappingURL=passkey-provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"passkey-provider.js","sourceRoot":"","sources":["../../src/identity/passkey-provider.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,EACL,6BAA6B,EAC7B,2BAA2B,EAC3B,4BAA4B,EAC5B,0BAA0B,GAM3B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,qBAAqB,EACrB,uBAAuB,EACvB,qBAAqB,EACrB,wBAAwB,EACxB,4BAA4B,EAC5B,4BAA4B,GAC7B,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAO5C,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAWhG,MAAM,cAAc;IACD,UAAU,GAAG,IAAI,GAAG,EAA0B,CAAC;IAEhE,GAAG,CAAC,SAAiB,EAAE,IAA0B;QAC/C,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,qBAAqB,EAAE,CAAC,CAAC;QACxF,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YAC3C,IAAI,KAAK,CAAC,SAAS,GAAG,GAAG;gBAAE,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACzD,CAAC;IACH,CAAC;IAED,0EAA0E;IAC1E,6EAA6E;IAC7E,gEAAgE;IAChE,OAAO,CAAC,SAAiB,EAAE,IAA0B;QACnD,MAAM,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC7C,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAClC,OAAO,KAAK,EAAE,IAAI,KAAK,IAAI,IAAI,KAAK,CAAC,SAAS,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;IAC/D,CAAC;CACF;AAED,MAAM,QAAQ,GAAG,CAAC,KAAc,EAAoC,EAAE,CACpE,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAEvE,MAAM,QAAQ,GAAG,KAAK,EAAE,OAAgB,EAAoC,EAAE;IAC5E,IAAI,CAAC;QACH,MAAM,IAAI,GAAY,MAAM,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;QAC/C,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;IACpC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,iBAAiB,GAAG,CAAC,KAAc,EAAiB,EAAE;IAC1D,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC3C,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,IACE,OAAO,CAAC,MAAM,GAAG,4BAA4B;QAC7C,OAAO,CAAC,MAAM,GAAG,4BAA4B,EAC7C,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC,CAAC;AAEF,8EAA8E;AAC9E,yEAAyE;AACzE,0EAA0E;AAC1E,yEAAyE;AACzE,2EAA2E;AAC3E,MAAM,SAAS,GAAG,CAChB,OAAgB,EAChB,SAA8B,EACW,EAAE;IAC3C,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,SAAS,EAAE,CAAC;IACxD,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IACtB,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;QACzB,IAAI,CAAC,GAAG,CAAC,QAAQ;YAAE,OAAO,IAAI,CAAC;QAC/B,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,EAAE,GAAG,CAAC,QAAQ,EAAE,CAAC;IACpD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC,CAAC;AAEF,6EAA6E;AAC7E,+EAA+E;AAC/E,6EAA6E;AAC7E,wCAAwC;AACxC,MAAM,sBAAsB,GAAG,CAAC,KAAc,EAAqC,EAAE,CACnF,QAAQ,CAAC,KAAK,CAAC,IAAI,OAAO,KAAK,CAAC,EAAE,KAAK,QAAQ,IAAI,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;AAC9E,MAAM,wBAAwB,GAAG,CAAC,KAAc,EAAuC,EAAE,CACvF,QAAQ,CAAC,KAAK,CAAC,IAAI,OAAO,KAAK,CAAC,EAAE,KAAK,QAAQ,IAAI,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;AAY9E,MAAM,kBAAkB,GAAG,CAAC,IAAsB,EAAQ,EAAE;IAC1D,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;IAEvB,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC,OAAO,EAAE,EAAE;QACjC,MAAM,QAAQ,GAAG,mBAAmB,CAAC,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;QAC3D,OAAO,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,IAAI,IAAI,EAAE,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,IAAI,CAAC,2BAA2B,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;QACtD,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC;YAC3B,OAAO,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,qBAAqB,EAAE,EAAE,qBAAqB,CAAC,CAAC;QAC/E,CAAC;QACD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,OAAO,CAAC,CAAC;QACrC,MAAM,QAAQ,GAAG,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAClD,IAAI,CAAC,QAAQ;YAAE,OAAO,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,kBAAkB,EAAE,EAAE,uBAAuB,CAAC,CAAC;QAC3F,MAAM,EAAE,GAAG,SAAS,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;QAC9C,IAAI,CAAC,EAAE;YAAE,OAAO,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,gBAAgB,EAAE,EAAE,uBAAuB,CAAC,CAAC;QACnF,MAAM,kBAAkB,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,aAAa,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;YAC1F,EAAE;SACH,CAAC,CAAC,CAAC;QACJ,MAAM,OAAO,GAAG,MAAM,2BAA2B,CAAC;YAChD,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,IAAI,EAAE,EAAE,CAAC,IAAI;YACb,QAAQ,EAAE,QAAQ;YAClB,kBAAkB;YAClB,sBAAsB,EAAE,EAAE,WAAW,EAAE,WAAW,EAAE,gBAAgB,EAAE,WAAW,EAAE;SACpF,CAAC,CAAC;QACH,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;QACnD,OAAO,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC/B,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,IAAI,CAAC,0BAA0B,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;QACrD,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC;YAC3B,OAAO,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,qBAAqB,EAAE,EAAE,qBAAqB,CAAC,CAAC;QAC/E,CAAC;QACD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,OAAO,CAAC,CAAC;QACrC,MAAM,QAAQ,GAAG,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAClD,IAAI,CAAC,QAAQ,IAAI,CAAC,sBAAsB,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;YACxD,OAAO,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,EAAE,uBAAuB,CAAC,CAAC;QAC1E,CAAC;QACD,MAAM,EAAE,GAAG,SAAS,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;QAC9C,IAAI,CAAC,EAAE;YAAE,OAAO,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,gBAAgB,EAAE,EAAE,uBAAuB,CAAC,CAAC;QACnF,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC/B,IAAI,QAAkD,CAAC;QACvD,IAAI,CAAC;YACH,QAAQ,GAAG,MAAM,0BAA0B,CAAC;gBAC1C,QAAQ;gBACR,iBAAiB,EAAE,CAAC,SAAS,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,SAAS,EAAE,UAAU,CAAC;gBAChF,cAAc,EAAE,EAAE,CAAC,MAAM;gBACzB,YAAY,EAAE,EAAE,CAAC,IAAI;gBACrB,uBAAuB,EAAE,IAAI;aAC9B,CAAC,CAAC;QACL,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,qBAAqB,EAAE,EAAE,uBAAuB,CAAC,CAAC;QACjF,CAAC;QACD,IAAI,CAAC,QAAQ,IAAI,CAAC,QAAQ,CAAC,QAAQ,IAAI,CAAC,QAAQ,CAAC,gBAAgB,EAAE,CAAC;YAClE,OAAO,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,qBAAqB,EAAE,EAAE,uBAAuB,CAAC,CAAC;QACjF,CAAC;QACD,MAAM,UAAU,GAAG,QAAQ,CAAC,gBAAgB,CAAC,UAAU,CAAC;QACxD,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;QACtC,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,QAAQ,EAAE,UAAU,CAAC,EAAE,CAAC,CAAC;QACtD,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC;YACvB,EAAE,EAAE,UAAU,CAAC,EAAE;YACjB,SAAS,EAAE,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC;YAC/D,OAAO,EAAE,UAAU,CAAC,OAAO;YAC3B,QAAQ;SACT,CAAC,CAAC;QACH,gBAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;QACjD,OAAO,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC1C,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,IAAI,CAAC,wBAAwB,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;QACnD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,OAAO,CAAC,CAAC;QACrC,MAAM,QAAQ,GAAG,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAClD,MAAM,EAAE,GAAG,SAAS,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;QAC9C,IAAI,CAAC,EAAE;YAAE,OAAO,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,gBAAgB,EAAE,EAAE,uBAAuB,CAAC,CAAC;QACnF,MAAM,gBAAgB,GAAG,QAAQ;YAC/B,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,aAAa,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;YAC3E,CAAC,CAAC,SAAS,CAAC;QACd,MAAM,OAAO,GAAG,MAAM,6BAA6B,CAAC;YAClD,IAAI,EAAE,EAAE,CAAC,IAAI;YACb,gBAAgB;YAChB,gBAAgB,EAAE,WAAW;SAC9B,CAAC,CAAC;QACH,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QAChD,OAAO,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC/B,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,IAAI,CAAC,uBAAuB,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;QAClD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,OAAO,CAAC,CAAC;QACrC,IAAI,CAAC,wBAAwB,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC7C,OAAO,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,EAAE,uBAAuB,CAAC,CAAC;QAC1E,CAAC;QACD,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC/B,MAAM,EAAE,GAAG,SAAS,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;QAC9C,IAAI,CAAC,EAAE;YAAE,OAAO,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,gBAAgB,EAAE,EAAE,uBAAuB,CAAC,CAAC;QACnF,MAAM,MAAM,GAAG,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;QACrD,IAAI,CAAC,MAAM;YAAE,OAAO,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,oBAAoB,EAAE,EAAE,uBAAuB,CAAC,CAAC;QAC3F,MAAM,UAAU,GAAuB;YACrC,EAAE,EAAE,MAAM,CAAC,EAAE;YACb,SAAS,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,QAAQ,CAAC;YAClD,OAAO,EAAE,MAAM,CAAC,OAAO;SACxB,CAAC;QACF,IAAI,QAAoD,CAAC;QACzD,IAAI,CAAC;YACH,QAAQ,GAAG,MAAM,4BAA4B,CAAC;gBAC5C,QAAQ;gBACR,iBAAiB,EAAE,CAAC,SAAS,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC;gBAC7E,cAAc,EAAE,EAAE,CAAC,MAAM;gBACzB,YAAY,EAAE,EAAE,CAAC,IAAI;gBACrB,UAAU;gBACV,uBAAuB,EAAE,IAAI;aAC9B,CAAC,CAAC;QACL,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,qBAAqB,EAAE,EAAE,uBAAuB,CAAC,CAAC;QACjF,CAAC;QACD,IAAI,CAAC,QAAQ,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,CAAC;YACpC,OAAO,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,qBAAqB,EAAE,EAAE,uBAAuB,CAAC,CAAC;QACjF,CAAC;QACD,IAAI,CAAC,eAAe,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,EAAE,QAAQ,CAAC,kBAAkB,CAAC,UAAU,CAAC,CAAC;QACtF,gBAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;QACxD,OAAO,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,IAAI,CAAC,iBAAiB,EAAE,CAAC,OAAO,EAAE,EAAE;QACtC,kBAAkB,CAAC,OAAO,CAAC,CAAC;QAC5B,OAAO,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;IAEH,OAAO,GAAG,CAAC;AACb,CAAC,CAAC;AAEF,6EAA6E;AAC7E,0EAA0E;AAC1E,0EAA0E;AAC1E,0EAA0E;AAC1E,yEAAyE;AACzE,wEAAwE;AACxE,MAAM,CAAC,MAAM,6BAA6B,GAAG,CAC3C,MAA6B,EAC7B,IAA0B,EACR,EAAE;IACpB,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,wBAAwB,CAAC;IACjE,MAAM,gBAAgB,GAAG,CAAC,MAAM,CAAC,YAAY,IAAI,MAAM,CAAC,KAAK,MAAM,CAAC;IACpE,MAAM,SAAS,GAAG,IAAI,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,YAAY,CAAC,CAAC,CAAC;IAC9E,MAAM,eAAe,GAAG,IAAI,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,kBAAkB,CAAC,CAAC,CAAC;IAChG,MAAM,UAAU,GAAG,IAAI,cAAc,EAAE,CAAC;IACxC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;IAE3B,OAAO;QACL,IAAI,EAAE,SAAS;QACf,mBAAmB,EAAE,IAAI;QACzB,aAAa,EAAE,MAAM,CAAC,aAAa,IAAI,IAAI;QAC3C,QAAQ,EAAE,CAAC,OAAgB,EAAmB,EAAE,CAAC,mBAAmB,CAAC,OAAO,EAAE,MAAM,CAAC;QACrF,MAAM,EAAE,GAAG,EAAE,CACX,kBAAkB,CAAC;YACjB,MAAM;YACN,gBAAgB;YAChB,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,SAAS;YACT,eAAe;YACf,UAAU;YACV,MAAM;SACP,CAAC;KACL,CAAC;AACJ,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy-allowlist.d.ts","sourceRoot":"","sources":["../../src/identity/proxy-allowlist.ts"],"names":[],"mappings":"AA2BA,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC;CACnC;AAQD,eAAO,MAAM,oBAAoB,GAAI,MAAM,MAAM,KAAG,cA6BnD,CAAC"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
const IPV4_MAPPED_PREFIX = "::ffff:";
|
|
3
|
+
// net.BlockList checks family-strict, so a dual-stack listener hands us an
|
|
4
|
+
// IPv4-mapped IPv6 address (::ffff:1.2.3.4) that an IPv4 subnet rule would
|
|
5
|
+
// miss. Normalize it back to the v4 form so one allowlist rule covers both.
|
|
6
|
+
const normalizeIp = (ip) => {
|
|
7
|
+
if (ip.startsWith(IPV4_MAPPED_PREFIX)) {
|
|
8
|
+
return { address: ip.slice(IPV4_MAPPED_PREFIX.length), family: "ipv4" };
|
|
9
|
+
}
|
|
10
|
+
return { address: ip, family: ip.includes(":") ? "ipv6" : "ipv4" };
|
|
11
|
+
};
|
|
12
|
+
const parseCidr = (cidr) => {
|
|
13
|
+
const slash = cidr.indexOf("/");
|
|
14
|
+
if (slash === -1)
|
|
15
|
+
return null;
|
|
16
|
+
const address = cidr.slice(0, slash);
|
|
17
|
+
const prefix = Number.parseInt(cidr.slice(slash + 1), 10);
|
|
18
|
+
if (!Number.isInteger(prefix) || prefix < 0)
|
|
19
|
+
return null;
|
|
20
|
+
const family = address.includes(":") ? "ipv6" : "ipv4";
|
|
21
|
+
if (prefix > (family === "ipv4" ? 32 : 128))
|
|
22
|
+
return null;
|
|
23
|
+
return { address, prefix, family };
|
|
24
|
+
};
|
|
25
|
+
// Build a source-IP allowlist for the `header` identity provider. `spec` is one
|
|
26
|
+
// of the shorthands `"loopback"` (127/8, ::1) or `"private"` (RFC1918, CGNAT,
|
|
27
|
+
// link-local, ULA — mirrors the network-policy private check), a CIDR string
|
|
28
|
+
// (`"10.0.0.0/8"`, `"::1/128"`), or a bare address. The provider only honors
|
|
29
|
+
// the identity header when the request's source IP is in this range, so a
|
|
30
|
+
// direct caller forging the header from outside the proxy is ignored.
|
|
31
|
+
export const createProxyAllowlist = (spec) => {
|
|
32
|
+
const list = new net.BlockList();
|
|
33
|
+
if (spec === "loopback") {
|
|
34
|
+
list.addSubnet("127.0.0.0", 8, "ipv4");
|
|
35
|
+
list.addAddress("::1", "ipv6");
|
|
36
|
+
}
|
|
37
|
+
else if (spec === "private") {
|
|
38
|
+
list.addSubnet("127.0.0.0", 8, "ipv4");
|
|
39
|
+
list.addSubnet("10.0.0.0", 8, "ipv4");
|
|
40
|
+
list.addSubnet("172.16.0.0", 12, "ipv4");
|
|
41
|
+
list.addSubnet("192.168.0.0", 16, "ipv4");
|
|
42
|
+
list.addSubnet("100.64.0.0", 10, "ipv4");
|
|
43
|
+
list.addSubnet("169.254.0.0", 16, "ipv4");
|
|
44
|
+
list.addAddress("::1", "ipv6");
|
|
45
|
+
list.addSubnet("fc00::", 7, "ipv6");
|
|
46
|
+
list.addSubnet("fe80::", 10, "ipv6");
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
const cidr = parseCidr(spec);
|
|
50
|
+
if (cidr) {
|
|
51
|
+
list.addSubnet(cidr.address, cidr.prefix, cidr.family);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
list.addAddress(spec, spec.includes(":") ? "ipv6" : "ipv4");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
contains: (ip) => {
|
|
59
|
+
const { address, family } = normalizeIp(ip);
|
|
60
|
+
return list.check(address, family);
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
//# sourceMappingURL=proxy-allowlist.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy-allowlist.js","sourceRoot":"","sources":["../../src/identity/proxy-allowlist.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Context } from "hono";
|
|
2
|
+
import type { MiddlewareHandler } from "hono";
|
|
3
|
+
import type { Identity, IdentityProvider, SessionOwner } from "./types.js";
|
|
4
|
+
export declare const getRequestSourceIp: (context: Context) => string | null;
|
|
5
|
+
export interface IdentityResolver {
|
|
6
|
+
resolve: (context: Context, sourceIp?: string | null) => Identity | null;
|
|
7
|
+
}
|
|
8
|
+
export declare const createIdentityResolver: (provider: IdentityProvider | null) => IdentityResolver;
|
|
9
|
+
export declare const toSessionOwner: (identity: Identity | null) => SessionOwner;
|
|
10
|
+
export declare const createAuthGateMiddleware: (provider: IdentityProvider | null, resolveIdentity: (context: Context, sourceIp?: string | null) => Identity | null) => MiddlewareHandler;
|
|
11
|
+
//# sourceMappingURL=resolve.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/identity/resolve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACpC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAI9C,OAAO,KAAK,EAAE,QAAQ,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAO3E,eAAO,MAAM,kBAAkB,GAAI,SAAS,OAAO,KAAG,MAAM,GAAG,IAO9D,CAAC;AAEF,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,KAAK,QAAQ,GAAG,IAAI,CAAC;CAC1E;AASD,eAAO,MAAM,sBAAsB,GAAI,UAAU,gBAAgB,GAAG,IAAI,KAAG,gBAGzE,CAAC;AAEH,eAAO,MAAM,cAAc,GAAI,UAAU,QAAQ,GAAG,IAAI,KAAG,YAC1B,CAAC;AAYlC,eAAO,MAAM,wBAAwB,GAEjC,UAAU,gBAAgB,GAAG,IAAI,EACjC,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,KAAK,QAAQ,GAAG,IAAI,KAC/E,iBAkBF,CAAC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { getConnInfo } from "@hono/node-server/conninfo";
|
|
2
|
+
import { HTTP_STATUS_UNAUTHORIZED } from "../constants.js";
|
|
3
|
+
import { timingSafeEqualString } from "../utils/timing-safe-equal.js";
|
|
4
|
+
// Best-effort source-IP read for an HTTP (non-WS) request. @hono/node-server's
|
|
5
|
+
// conninfo reads the underlying socket's remoteAddress. Returns null when the
|
|
6
|
+
// adapter doesn't expose it (or the helper throws — never 500 a request over
|
|
7
|
+
// an identity read) so the caller falls back to "untrusted source" and ignores
|
|
8
|
+
// any identity header.
|
|
9
|
+
export const getRequestSourceIp = (context) => {
|
|
10
|
+
try {
|
|
11
|
+
const address = getConnInfo(context).remote.address;
|
|
12
|
+
return typeof address === "string" ? address : null;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
// Build the per-request identity resolver. With no provider configured it
|
|
19
|
+
// resolves to `null` for every request — the operator/legacy tier — so every
|
|
20
|
+
// request is the single authority and the registry stays unscoped,
|
|
21
|
+
// byte-identical to the no-auth behavior. Otherwise it delegates to the
|
|
22
|
+
// provider, injecting the caller-resolved source IP: the WS upgrade reads it
|
|
23
|
+
// from the raw socket (more authoritative than conninfo at upgrade time), HTTP
|
|
24
|
+
// routes via conninfo.
|
|
25
|
+
export const createIdentityResolver = (provider) => ({
|
|
26
|
+
resolve: (context, sourceIp = null) => provider ? provider.identify(context, sourceIp) : null,
|
|
27
|
+
});
|
|
28
|
+
export const toSessionOwner = (identity) => identity ? identity.user : null;
|
|
29
|
+
// The auth gate for providers that own their own login (passkey/oidc): reject a
|
|
30
|
+
// request with no valid session at the door (401) so it never reaches the
|
|
31
|
+
// session registry — unlike `header`, whose no-header case is the operator
|
|
32
|
+
// tier (denyUnauthenticated: false), there's no external proxy to vouch for an
|
|
33
|
+
// unauthenticated caller here, so silence can't mean admin. Exempts
|
|
34
|
+
// `/api/health` (readiness) and everything outside `/api` and `/ws` (the static
|
|
35
|
+
// terminal app + the `/auth` login flow must load before there's a session). A
|
|
36
|
+
// request carrying the daemon's operator bearer token (the CLI) is admitted as
|
|
37
|
+
// the operator tier — full access, no session — since the CLI can't run a
|
|
38
|
+
// WebAuthn/OIDC ceremony.
|
|
39
|
+
export const createAuthGateMiddleware = (provider, resolveIdentity) => async (context, next) => {
|
|
40
|
+
if (!provider?.denyUnauthenticated)
|
|
41
|
+
return await next();
|
|
42
|
+
const requestPath = context.req.path;
|
|
43
|
+
const isProtected = (requestPath === "/ws" || requestPath.startsWith("/api/")) && requestPath !== "/api/health";
|
|
44
|
+
if (!isProtected)
|
|
45
|
+
return await next();
|
|
46
|
+
const authorization = context.req.header("authorization");
|
|
47
|
+
if (provider.operatorToken &&
|
|
48
|
+
authorization &&
|
|
49
|
+
timingSafeEqualString(authorization, `Bearer ${provider.operatorToken}`)) {
|
|
50
|
+
return await next();
|
|
51
|
+
}
|
|
52
|
+
const identity = resolveIdentity(context, getRequestSourceIp(context));
|
|
53
|
+
if (!identity)
|
|
54
|
+
return context.json({ error: "unauthorized" }, HTTP_STATUS_UNAUTHORIZED);
|
|
55
|
+
await next();
|
|
56
|
+
};
|
|
57
|
+
//# sourceMappingURL=resolve.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../src/identity/resolve.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,EAAE,wBAAwB,EAAE,MAAM,iBAAiB,CAAC;AAC3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAGtE,+EAA+E;AAC/E,8EAA8E;AAC9E,6EAA6E;AAC7E,+EAA+E;AAC/E,uBAAuB;AACvB,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,OAAgB,EAAiB,EAAE;IACpE,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC;QACpD,OAAO,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;IACtD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC,CAAC;AAMF,0EAA0E;AAC1E,6EAA6E;AAC7E,mEAAmE;AACnE,wEAAwE;AACxE,6EAA6E;AAC7E,+EAA+E;AAC/E,uBAAuB;AACvB,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,QAAiC,EAAoB,EAAE,CAAC,CAAC;IAC9F,OAAO,EAAE,CAAC,OAAgB,EAAE,WAA0B,IAAI,EAAmB,EAAE,CAC7E,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI;CACzD,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,QAAyB,EAAgB,EAAE,CACxE,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;AAElC,gFAAgF;AAChF,0EAA0E;AAC1E,2EAA2E;AAC3E,+EAA+E;AAC/E,oEAAoE;AACpE,gFAAgF;AAChF,+EAA+E;AAC/E,+EAA+E;AAC/E,0EAA0E;AAC1E,0BAA0B;AAC1B,MAAM,CAAC,MAAM,wBAAwB,GACnC,CACE,QAAiC,EACjC,eAAgF,EAC7D,EAAE,CACvB,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;IACtB,IAAI,CAAC,QAAQ,EAAE,mBAAmB;QAAE,OAAO,MAAM,IAAI,EAAE,CAAC;IACxD,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC;IACrC,MAAM,WAAW,GACf,CAAC,WAAW,KAAK,KAAK,IAAI,WAAW,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,IAAI,WAAW,KAAK,aAAa,CAAC;IAC9F,IAAI,CAAC,WAAW;QAAE,OAAO,MAAM,IAAI,EAAE,CAAC;IACtC,MAAM,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;IAC1D,IACE,QAAQ,CAAC,aAAa;QACtB,aAAa;QACb,qBAAqB,CAAC,aAAa,EAAE,UAAU,QAAQ,CAAC,aAAa,EAAE,CAAC,EACxE,CAAC;QACD,OAAO,MAAM,IAAI,EAAE,CAAC;IACtB,CAAC;IACD,MAAM,QAAQ,GAAG,eAAe,CAAC,OAAO,EAAE,kBAAkB,CAAC,OAAO,CAAC,CAAC,CAAC;IACvE,IAAI,CAAC,QAAQ;QAAE,OAAO,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,EAAE,wBAAwB,CAAC,CAAC;IACxF,MAAM,IAAI,EAAE,CAAC;AACf,CAAC,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Context } from "hono";
|
|
2
|
+
import type { Identity } from "./types.js";
|
|
3
|
+
export declare const generateAuthSecret: () => string;
|
|
4
|
+
export declare const loadOrCreateAuthSecret: (filePath: string) => string;
|
|
5
|
+
export declare const signSessionToken: (secret: string, user: string) => string;
|
|
6
|
+
export declare const verifySessionToken: (secret: string, token: string) => string | null;
|
|
7
|
+
export declare const setSessionCookie: (context: Context, secret: string, user: string) => void;
|
|
8
|
+
export declare const clearSessionCookie: (context: Context) => void;
|
|
9
|
+
export declare const readSessionIdentity: (context: Context, secret: string) => Identity | null;
|
|
10
|
+
//# sourceMappingURL=session-cookie.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-cookie.d.ts","sourceRoot":"","sources":["../../src/identity/session-cookie.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAGpC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAe3C,eAAO,MAAM,kBAAkB,QAAO,MACgB,CAAC;AAKvD,eAAO,MAAM,sBAAsB,GAAI,UAAU,MAAM,KAAG,MAazD,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAAI,QAAQ,MAAM,EAAE,MAAM,MAAM,KAAG,MAO/D,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,QAAQ,MAAM,EAAE,OAAO,MAAM,KAAG,MAAM,GAAG,IAmB3E,CAAC;AAsBF,eAAO,MAAM,gBAAgB,GAAI,SAAS,OAAO,EAAE,QAAQ,MAAM,EAAE,MAAM,MAAM,KAAG,IAOjF,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,SAAS,OAAO,KAAG,IAErD,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAAI,SAAS,OAAO,EAAE,QAAQ,MAAM,KAAG,QAAQ,GAAG,IAKjF,CAAC"}
|