@aventara/access 0.0.0-stage → 1.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +877 -0
- package/LICENSE +91 -0
- package/LICENSE-ADDITIONAL-PERMISSION.md +9 -0
- package/README.md +2536 -2
- package/dist/access/access-options.d.ts +74 -0
- package/dist/access/access-options.js +5 -0
- package/dist/access/access.error.d.ts +35 -0
- package/dist/access/access.error.js +11 -0
- package/dist/access/access.factory.d.ts +275 -0
- package/dist/access/access.factory.js +440 -0
- package/dist/access/better-auth.hardening.d.ts +120 -0
- package/dist/access/better-auth.hardening.js +420 -0
- package/dist/access/better-auth.loader.d.ts +67 -0
- package/dist/access/better-auth.loader.js +65 -0
- package/dist/access/better-auth.range.d.ts +9 -0
- package/dist/access/better-auth.range.js +28 -0
- package/dist/access/better-auth.require.d.ts +90 -0
- package/dist/access/better-auth.require.js +6 -0
- package/dist/access/better-auth.require.node.d.ts +7 -0
- package/dist/access/better-auth.require.node.js +52 -0
- package/dist/access/claims.mapper.d.ts +79 -0
- package/dist/access/claims.mapper.js +152 -0
- package/dist/access/key-set.cache.d.ts +10 -0
- package/dist/access/key-set.cache.js +63 -0
- package/dist/access/policies-block.checker.d.ts +64 -0
- package/dist/access/policies-block.checker.js +138 -0
- package/dist/access/principal.subject.d.ts +22 -0
- package/dist/access/principal.subject.js +89 -0
- package/dist/access/profile.policy.d.ts +74 -0
- package/dist/access/profile.policy.js +110 -0
- package/dist/access/request.state.d.ts +89 -0
- package/dist/access/request.state.js +266 -0
- package/dist/access/session.hooks.d.ts +114 -0
- package/dist/access/session.hooks.js +258 -0
- package/dist/access/token.verifier.d.ts +35 -0
- package/dist/access/token.verifier.js +69 -0
- package/dist/access-schema.d.ts +48 -0
- package/dist/access-schema.js +5 -0
- package/dist/access-warning.emitter.d.ts +4 -0
- package/dist/access-warning.emitter.js +24 -0
- package/dist/access.builder.d.ts +340 -0
- package/dist/access.builder.js +75 -0
- package/dist/access.engine.d.ts +155 -0
- package/dist/access.engine.js +477 -0
- package/dist/audit/audit-event.d.ts +135 -0
- package/dist/audit/audit-event.js +8 -0
- package/dist/audit/audit.log.d.ts +122 -0
- package/dist/audit/audit.log.js +345 -0
- package/dist/audit/audit.schema.d.ts +143 -0
- package/dist/audit/audit.schema.js +50 -0
- package/dist/auth/index.d.ts +12 -0
- package/dist/auth/index.js +2 -0
- package/dist/aventara-where.translator.d.ts +19 -0
- package/dist/aventara-where.translator.js +87 -0
- package/dist/beacon/beacon.check.d.ts +59 -0
- package/dist/beacon/beacon.check.js +128 -0
- package/dist/beacon/beacon.schema.d.ts +41 -0
- package/dist/beacon/beacon.schema.js +48 -0
- package/dist/beacon/beacon.store.d.ts +29 -0
- package/dist/beacon/beacon.store.js +78 -0
- package/dist/better-auth/access.client-plugin.d.ts +75 -0
- package/dist/better-auth/access.client-plugin.js +133 -0
- package/dist/better-auth/access.plugin.d.ts +164 -0
- package/dist/better-auth/access.plugin.js +348 -0
- package/dist/better-auth/access.provider.d.ts +80 -0
- package/dist/better-auth/access.provider.js +100 -0
- package/dist/better-auth/index.d.ts +7 -0
- package/dist/better-auth/index.js +4 -0
- package/dist/better-auth/organization.bridge.d.ts +66 -0
- package/dist/better-auth/organization.bridge.js +144 -0
- package/dist/canonical-json.writer.d.ts +12 -0
- package/dist/canonical-json.writer.js +30 -0
- package/dist/cli/avaccess.bin.d.ts +2 -0
- package/dist/cli/avaccess.bin.js +26 -0
- package/dist/cli/avaccess.program.d.ts +15 -0
- package/dist/cli/avaccess.program.js +166 -0
- package/dist/cli/beacon-pins.file.d.ts +14 -0
- package/dist/cli/beacon-pins.file.js +62 -0
- package/dist/cli/beacon.loader.d.ts +20 -0
- package/dist/cli/beacon.loader.js +71 -0
- package/dist/cli/cli.context.d.ts +49 -0
- package/dist/cli/cli.context.js +66 -0
- package/dist/cli/diff.command.d.ts +2 -0
- package/dist/cli/diff.command.js +66 -0
- package/dist/cli/generate.command.d.ts +2 -0
- package/dist/cli/generate.command.js +82 -0
- package/dist/cli/keys.command.d.ts +2 -0
- package/dist/cli/keys.command.js +96 -0
- package/dist/cli/lock.file.d.ts +31 -0
- package/dist/cli/lock.file.js +181 -0
- package/dist/cli/module.loader.d.ts +13 -0
- package/dist/cli/module.loader.js +63 -0
- package/dist/cli/policies.diff.d.ts +16 -0
- package/dist/cli/policies.diff.js +101 -0
- package/dist/cli/pull.command.d.ts +2 -0
- package/dist/cli/pull.command.js +234 -0
- package/dist/cli/push.command.d.ts +2 -0
- package/dist/cli/push.command.js +59 -0
- package/dist/cli/registry.client.d.ts +63 -0
- package/dist/cli/registry.client.js +287 -0
- package/dist/cli/schema.command.d.ts +2 -0
- package/dist/cli/schema.command.js +60 -0
- package/dist/cli/serve.command.d.ts +6 -0
- package/dist/cli/serve.command.js +200 -0
- package/dist/cli/status.command.d.ts +2 -0
- package/dist/cli/status.command.js +31 -0
- package/dist/cli/translator.flag.d.ts +6 -0
- package/dist/cli/translator.flag.js +24 -0
- package/dist/cli/verify.command.d.ts +2 -0
- package/dist/cli/verify.command.js +40 -0
- package/dist/client/access-client.factory.d.ts +92 -0
- package/dist/client/access-client.factory.js +269 -0
- package/dist/client/access-verbs.schema.d.ts +66 -0
- package/dist/client/access-verbs.schema.js +159 -0
- package/dist/client/verb-rate.limiter.d.ts +116 -0
- package/dist/client/verb-rate.limiter.js +358 -0
- package/dist/foreign/foreign-facts.d.ts +30 -0
- package/dist/foreign/foreign-facts.js +75 -0
- package/dist/foreign/foreign.merger.d.ts +10 -0
- package/dist/foreign/foreign.merger.js +52 -0
- package/dist/foreign-references.check.d.ts +6 -0
- package/dist/foreign-references.check.js +131 -0
- package/dist/generator/client.emitter.d.ts +28 -0
- package/dist/generator/client.emitter.js +411 -0
- package/dist/generator/copied-modules.reader.d.ts +14 -0
- package/dist/generator/copied-modules.reader.js +80 -0
- package/dist/generator/namespace.facts.d.ts +28 -0
- package/dist/generator/namespace.facts.js +32 -0
- package/dist/generator/orm.translator.d.ts +48 -0
- package/dist/generator/orm.translator.js +29 -0
- package/dist/generator/prisma7/prisma-schema.reader.d.ts +9 -0
- package/dist/generator/prisma7/prisma-schema.reader.js +394 -0
- package/dist/generator/prisma7/prisma-where.translator.d.ts +73 -0
- package/dist/generator/prisma7/prisma-where.translator.js +138 -0
- package/dist/generator/prisma7/prisma7.translator.d.ts +2 -0
- package/dist/generator/prisma7/prisma7.translator.js +87 -0
- package/dist/generator/schema.emitter.d.ts +14 -0
- package/dist/generator/schema.emitter.js +23 -0
- package/dist/generator/tsconfig.reader.d.ts +44 -0
- package/dist/generator/tsconfig.reader.js +110 -0
- package/dist/generator/typescript-literal.writer.d.ts +5 -0
- package/dist/generator/typescript-literal.writer.js +42 -0
- package/dist/guardrails/guardrails-contract.d.ts +45 -0
- package/dist/guardrails/guardrails-contract.js +12 -0
- package/dist/guardrails/guardrails.builder.d.ts +48 -0
- package/dist/guardrails/guardrails.builder.js +32 -0
- package/dist/guardrails/guardrails.compiler.d.ts +10 -0
- package/dist/guardrails/guardrails.compiler.js +254 -0
- package/dist/guardrails/guardrails.composer.d.ts +7 -0
- package/dist/guardrails/guardrails.composer.js +95 -0
- package/dist/guardrails/guardrails.hash.d.ts +10 -0
- package/dist/guardrails/guardrails.hash.js +38 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +6 -0
- package/dist/kiosk/kiosk-verbs.schema.d.ts +19 -0
- package/dist/kiosk/kiosk-verbs.schema.js +61 -0
- package/dist/kiosk/kiosk.plugin.d.ts +217 -0
- package/dist/kiosk/kiosk.plugin.js +877 -0
- package/dist/kiosk/kiosk.schema.d.ts +176 -0
- package/dist/kiosk/kiosk.schema.js +101 -0
- package/dist/kiosk/kiosk.store.d.ts +171 -0
- package/dist/kiosk/kiosk.store.js +478 -0
- package/dist/kiosk/lan-range.policy.d.ts +31 -0
- package/dist/kiosk/lan-range.policy.js +113 -0
- package/dist/kiosk/pin.policy.d.ts +9 -0
- package/dist/kiosk/pin.policy.js +143 -0
- package/dist/kiosk/session-use.marker.d.ts +4 -0
- package/dist/kiosk/session-use.marker.js +16 -0
- package/dist/lock/lock-contract.d.ts +38 -0
- package/dist/lock/lock-contract.js +67 -0
- package/dist/lock/lock.verifier.d.ts +27 -0
- package/dist/lock/lock.verifier.js +62 -0
- package/dist/mcp/agent-scope.gate.d.ts +13 -0
- package/dist/mcp/agent-scope.gate.js +75 -0
- package/dist/mcp/agent.grant.d.ts +40 -0
- package/dist/mcp/agent.grant.js +92 -0
- package/dist/mcp/mcp-options.d.ts +60 -0
- package/dist/mcp/mcp-options.js +72 -0
- package/dist/oauth/access-token.verifier.d.ts +66 -0
- package/dist/oauth/access-token.verifier.js +201 -0
- package/dist/oauth/oauth-provider.hardening.d.ts +62 -0
- package/dist/oauth/oauth-provider.hardening.js +261 -0
- package/dist/passwords/argon2id.hasher.d.ts +97 -0
- package/dist/passwords/argon2id.hasher.js +256 -0
- package/dist/passwords/node-rs-argon2.adapter.d.ts +25 -0
- package/dist/passwords/node-rs-argon2.adapter.js +42 -0
- package/dist/passwords/passwords.options.d.ts +79 -0
- package/dist/passwords/passwords.options.js +212 -0
- package/dist/policies-contract.d.ts +219 -0
- package/dist/policies-contract.js +8 -0
- package/dist/policies.compiler.d.ts +38 -0
- package/dist/policies.compiler.js +1433 -0
- package/dist/policies.decider.d.ts +103 -0
- package/dist/policies.decider.js +390 -0
- package/dist/policies.hash.d.ts +10 -0
- package/dist/policies.hash.js +54 -0
- package/dist/predicate.evaluator.d.ts +62 -0
- package/dist/predicate.evaluator.js +378 -0
- package/dist/random-secret.generator.d.ts +2 -0
- package/dist/random-secret.generator.js +15 -0
- package/dist/read-governed-unique.check.d.ts +3 -0
- package/dist/read-governed-unique.check.js +21 -0
- package/dist/reference-reads.check.d.ts +16 -0
- package/dist/reference-reads.check.js +158 -0
- package/dist/registry/access.registry.d.ts +173 -0
- package/dist/registry/access.registry.js +865 -0
- package/dist/registry/beacon.ledger.d.ts +30 -0
- package/dist/registry/beacon.ledger.js +93 -0
- package/dist/registry/ownership.check.d.ts +54 -0
- package/dist/registry/ownership.check.js +221 -0
- package/dist/registry/registry.endpoints.d.ts +50 -0
- package/dist/registry/registry.endpoints.js +193 -0
- package/dist/registry/registry.key.d.ts +7 -0
- package/dist/registry/registry.key.js +13 -0
- package/dist/registry/registry.options.d.ts +13 -0
- package/dist/registry/registry.options.js +28 -0
- package/dist/registry/registry.refusal.d.ts +6 -0
- package/dist/registry/registry.refusal.js +11 -0
- package/dist/registry/registry.schema.d.ts +238 -0
- package/dist/registry/registry.schema.js +127 -0
- package/dist/resources/auth-tables.catalog.d.ts +96 -0
- package/dist/resources/auth-tables.catalog.js +110 -0
- package/dist/resources/auth-tables.guard.d.ts +106 -0
- package/dist/resources/auth-tables.guard.js +462 -0
- package/dist/scope.resolver.d.ts +144 -0
- package/dist/scope.resolver.js +529 -0
- package/dist/sha256.digest.d.ts +2 -0
- package/dist/sha256.digest.js +81 -0
- package/dist/standalone/access.server.d.ts +41 -0
- package/dist/standalone/access.server.js +301 -0
- package/dist/standalone/index.d.ts +1 -0
- package/dist/standalone/index.js +1 -0
- package/package.json +102 -3
- package/skill/SKILL.md +170 -0
- package/skill/reference/callers.md +99 -0
- package/skill/reference/errors.md +89 -0
- package/skill/reference/recipes.md +281 -0
- package/skill/reference/rules.md +153 -0
- package/skill/reference/setup.md +334 -0
- package/src/aventara-where.translator.ts +116 -0
- package/src/beacon/beacon.check.ts +204 -0
- package/src/canonical-json.writer.ts +51 -0
- package/src/generator/prisma7/prisma-where.translator.ts +243 -0
- package/src/policies-contract.ts +215 -0
- package/src/policies.decider.ts +662 -0
- package/src/predicate.evaluator.ts +574 -0
- package/src/scope.resolver.ts +819 -0
- package/src/sha256.digest.ts +85 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// A request's state, `{ me }`: who the request's bearer token or session cookie says
|
|
2
|
+
// the caller is, verified, and mapped to the app's principal. `createAccess`'s
|
|
3
|
+
// `state` and `accessOf(auth).state` are this one function over their own Better
|
|
4
|
+
// Auth instance, so both ways in read a request alike: the bearer token against the
|
|
5
|
+
// key set, never a cookie beside an `Authorization` header, and a session cookie only
|
|
6
|
+
// from a site Better Auth trusts.
|
|
7
|
+
//
|
|
8
|
+
// A bearer token may also be an access token Better Auth's OAuth provider issued for
|
|
9
|
+
// one resource (`typ: "at+jwt"`), read by the verifier of that resource alone
|
|
10
|
+
// (`oauth/access-token.verifier.ts`), never as a session token:
|
|
11
|
+
//
|
|
12
|
+
// - with `mcp`, one for the MCP endpoint: an agent's, whose principal carries what the
|
|
13
|
+
// token grants (`mcp/agent.grant.ts`), which the policy holds it to before the rules
|
|
14
|
+
// (`mcp/agent-scope.gate.ts`), and which `agentOf(me)` reads;
|
|
15
|
+
// - with `audience` (the service's opt-in), one addressed to this service, and carrying
|
|
16
|
+
// one of Access's scopes (`aventara:read`, `aventara:write`): the user's, held to
|
|
17
|
+
// those scopes under every origin. One with neither is nobody's. The service then
|
|
18
|
+
// takes no other bearer token: the one-org token `GET /token` issues (its audience
|
|
19
|
+
// the issuer) is refused.
|
|
20
|
+
import { decodeProtectedHeader } from "jose";
|
|
21
|
+
import { mayChange, SESSION_USE } from "../kiosk/session-use.marker.js";
|
|
22
|
+
import { hasAventaraScope, withGrant, } from "../mcp/agent.grant.js";
|
|
23
|
+
import { carrySubject, claimReturned } from "./principal.subject.js";
|
|
24
|
+
import { readsSessionCookie, } from "./profile.policy.js";
|
|
25
|
+
import { isAccessTokenType } from "./token.verifier.js";
|
|
26
|
+
const bearerOf = (headers) => /^Bearer[ \t]+([^ \t]+)[ \t]*$/i.exec(headers.get("authorization") ?? "")?.[1];
|
|
27
|
+
/**
|
|
28
|
+
* Whether a request may be read by its session cookie, where the browser could have
|
|
29
|
+
* sent it from another site: its `Origin` is one Better Auth trusts, or, without
|
|
30
|
+
* one, the browser says it is not cross-site (`Sec-Fetch-Site` `same-origin` or
|
|
31
|
+
* `none`). A request with neither header is no browser's, and passes.
|
|
32
|
+
*/
|
|
33
|
+
const fromTrustedSite = (headers, trusted) => {
|
|
34
|
+
const origin = headers.get("origin");
|
|
35
|
+
if (origin !== null) {
|
|
36
|
+
return origin !== "null" && trusted(origin);
|
|
37
|
+
}
|
|
38
|
+
const site = headers.get("sec-fetch-site");
|
|
39
|
+
return site === null || site === "same-origin" || site === "none";
|
|
40
|
+
};
|
|
41
|
+
/** The methods that change nothing: a cookie on them is read whatever site sent it (with `sameSite: "lax"`, a browser sends it from another site only on a top-level `GET`). */
|
|
42
|
+
const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
|
|
43
|
+
/** The method the client sent, from the host's request (Express, Fastify or a Web `Request`); `undefined` when there is none. */
|
|
44
|
+
const methodOf = (request) => {
|
|
45
|
+
const { method, originalMethod } = (request ?? {});
|
|
46
|
+
// Express's method-override keeps the method the client sent here.
|
|
47
|
+
const sent = typeof originalMethod === "string" ? originalMethod : method;
|
|
48
|
+
return typeof sent === "string" ? sent.toUpperCase() : undefined;
|
|
49
|
+
};
|
|
50
|
+
const headersOf = (given) => {
|
|
51
|
+
const headers = new Headers();
|
|
52
|
+
given.forEach((value, name) => {
|
|
53
|
+
headers.append(name, value);
|
|
54
|
+
});
|
|
55
|
+
return headers;
|
|
56
|
+
};
|
|
57
|
+
/** Whether a JWT's header names an OAuth access token; `false` for anything that is not a JWT. */
|
|
58
|
+
const isAccessToken = (token) => {
|
|
59
|
+
try {
|
|
60
|
+
return isAccessTokenType(decodeProtectedHeader(token).typ);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Who Access's own `GET /me` route answers a request as: the identity of a session
|
|
68
|
+
* token, a session cookie or a token addressed to the service; `null` for nobody, and
|
|
69
|
+
* for an agent's access token, which opens nothing over HTTP but the MCP endpoint.
|
|
70
|
+
*/
|
|
71
|
+
export const meOf = (caller) => caller === null || caller.grant?.via === "mcp" ? null : caller.identity;
|
|
72
|
+
/**
|
|
73
|
+
* Who `access.client.verbs.me` answers a request as: the identity `{ sub, claims }`
|
|
74
|
+
* of any credential the state reads, an agent's included (the framework's MCP `me`
|
|
75
|
+
* tool calls it in-process with the agent's headers). A token's identity is a copy
|
|
76
|
+
* carrying what it grants, so `agentOf` tells an agent's apart.
|
|
77
|
+
*/
|
|
78
|
+
export const verbMeOf = (caller) => caller === null
|
|
79
|
+
? null
|
|
80
|
+
: caller.grant === undefined
|
|
81
|
+
? caller.identity
|
|
82
|
+
: withGrant(caller.identity, caller.grant);
|
|
83
|
+
/**
|
|
84
|
+
* A request's identity over one Better Auth instance (`auth`, built on first use):
|
|
85
|
+
* from a bearer token the verifier accepts (an agent's access token, the agent
|
|
86
|
+
* verifier, when `mcp` is on), or (profile `web`) from the session Better Auth's
|
|
87
|
+
* store holds for the request's cookie, with the claims `claimsFor` gives that
|
|
88
|
+
* session.
|
|
89
|
+
*/
|
|
90
|
+
export const requestIdentity = (input) => {
|
|
91
|
+
const { auth, verifier, agents, audience, kind, crossSite, claimsFor } = input;
|
|
92
|
+
const identityOf = async (headers, request) => {
|
|
93
|
+
// An Authorization header names the credential: a bearer token, or nothing
|
|
94
|
+
// Access reads. The session cookie beside it is not a fallback.
|
|
95
|
+
if (headers.has("authorization")) {
|
|
96
|
+
const token = bearerOf(headers);
|
|
97
|
+
if (token === undefined) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
// An access token is read by the verifiers of the resources this instance
|
|
101
|
+
// reads tokens for, never by the session verifier (which refuses one too).
|
|
102
|
+
if (isAccessToken(token)) {
|
|
103
|
+
const agent = await agents?.verify(token);
|
|
104
|
+
if (agent !== undefined && agent !== null) {
|
|
105
|
+
return {
|
|
106
|
+
identity: agent.identity,
|
|
107
|
+
grant: { ...agent.grant, via: "mcp" },
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
const own = await audience?.verify(token);
|
|
111
|
+
// Scopes narrow, never widen: a token addressed to the service with none of
|
|
112
|
+
// Access's scopes grants nothing, and is nobody's.
|
|
113
|
+
return own === undefined ||
|
|
114
|
+
own === null ||
|
|
115
|
+
!hasAventaraScope(own.grant.scopes)
|
|
116
|
+
? null
|
|
117
|
+
: {
|
|
118
|
+
identity: own.identity,
|
|
119
|
+
grant: { ...own.grant, via: "audience" },
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
// A service that opts in to its own audience takes no one-org token.
|
|
123
|
+
if (audience !== undefined) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
const identity = await verifier.verify(token);
|
|
127
|
+
return identity === null ? null : { identity };
|
|
128
|
+
}
|
|
129
|
+
if (!readsSessionCookie(kind) || !headers.has("cookie")) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
// A cookie counts only from a site Better Auth trusts: on every request that
|
|
133
|
+
// may change something (any method but GET, HEAD and OPTIONS, and a request
|
|
134
|
+
// whose method the host did not pass), and with `sameSite: "none"`, on all.
|
|
135
|
+
const method = methodOf(request);
|
|
136
|
+
if (crossSite || method === undefined || !SAFE_METHODS.has(method)) {
|
|
137
|
+
const context = (await auth().$context);
|
|
138
|
+
if (!fromTrustedSite(headers, (origin) => context.isTrustedOrigin(origin))) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// The store, never the cookie cache: a revoked session is gone at once.
|
|
143
|
+
const session = await auth().api.getSession({
|
|
144
|
+
headers: headersOf(headers),
|
|
145
|
+
query: { disableCookieCache: true },
|
|
146
|
+
// A write is the till's use of its session (the kiosk's idle timer); a read is not.
|
|
147
|
+
...(mayChange(method) ? { [SESSION_USE]: true } : {}),
|
|
148
|
+
});
|
|
149
|
+
if (session === null) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
identity: { sub: session.user.id, claims: await claimsFor(session) },
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
return identityOf;
|
|
157
|
+
};
|
|
158
|
+
/**
|
|
159
|
+
* The request state over a request's identity (`requestIdentity`): `me` is the
|
|
160
|
+
* identity, or what the app's `principal` maps it to, with the `sub` carried on it
|
|
161
|
+
* for the audit, and, for an OAuth access token, a copy of it carrying what the
|
|
162
|
+
* token grants. The identity `principal` is given for such a token carries the grant
|
|
163
|
+
* too, so `agentOf(identity)` tells the app an agent's request while it maps it.
|
|
164
|
+
*/
|
|
165
|
+
export const requestState = (identityOf, principal) => {
|
|
166
|
+
return async ({ headers, request }) => {
|
|
167
|
+
const caller = await identityOf(headers, request);
|
|
168
|
+
if (caller === null) {
|
|
169
|
+
return stateOf(null);
|
|
170
|
+
}
|
|
171
|
+
const { grant } = caller;
|
|
172
|
+
const identity = grant === undefined
|
|
173
|
+
? caller.identity
|
|
174
|
+
: withGrant(caller.identity, grant);
|
|
175
|
+
const mapped = principal === undefined ? identity : await principal(identity);
|
|
176
|
+
// One object handed out for two users is refused (a cache keyed wrongly would
|
|
177
|
+
// serve one as the other), without writing to it.
|
|
178
|
+
claimReturned(mapped, identity.sub);
|
|
179
|
+
// A copy of the plain data: the app's own objects are never frozen.
|
|
180
|
+
const made = new Map();
|
|
181
|
+
const copied = plainCopy(mapped, made);
|
|
182
|
+
const me = grant === undefined ? copied : withGrant(copied, grant);
|
|
183
|
+
// Access's own objects: the copies, and the one `withGrant` makes. A value the
|
|
184
|
+
// app returned that is not plain data (a class instance, a `Map`) is `me` as given.
|
|
185
|
+
const ours = typeof me === "object" &&
|
|
186
|
+
me !== null &&
|
|
187
|
+
(grant !== undefined || made.get(mapped) === me);
|
|
188
|
+
// The `sub` travels with `me`: whichever instance the host asks for the
|
|
189
|
+
// policy (under Next, another bundle's) reads it for the audit's `userId`.
|
|
190
|
+
carrySubject(me, identity.sub, ours);
|
|
191
|
+
for (const copy of made.values()) {
|
|
192
|
+
Object.freeze(copy);
|
|
193
|
+
}
|
|
194
|
+
if (ours) {
|
|
195
|
+
Object.freeze(me);
|
|
196
|
+
}
|
|
197
|
+
return stateOf(me ?? null);
|
|
198
|
+
};
|
|
199
|
+
};
|
|
200
|
+
/**
|
|
201
|
+
* The state over `me`: `me` is a non-writable member of the state, and nothing of
|
|
202
|
+
* its plain data changes once it is derived, so no code that runs during the request
|
|
203
|
+
* (a pipeline, a hook, a computed field) can make the policy see another principal.
|
|
204
|
+
*/
|
|
205
|
+
const stateOf = (me) => Object.defineProperty({}, "me", {
|
|
206
|
+
value: me,
|
|
207
|
+
enumerable: true,
|
|
208
|
+
writable: false,
|
|
209
|
+
configurable: false,
|
|
210
|
+
});
|
|
211
|
+
/**
|
|
212
|
+
* Plain data: an array, or an object made by `{}`, JSON or `Object.create(null)`. A
|
|
213
|
+
* class instance is not: its `#private` fields cannot be copied, so it is kept as
|
|
214
|
+
* the app gave it, as a `Map` is.
|
|
215
|
+
*/
|
|
216
|
+
const isPlainData = (value) => {
|
|
217
|
+
const prototype = Object.getPrototypeOf(value);
|
|
218
|
+
return Array.isArray(value)
|
|
219
|
+
? prototype === Array.prototype
|
|
220
|
+
: prototype === Object.prototype || prototype === null;
|
|
221
|
+
};
|
|
222
|
+
/**
|
|
223
|
+
* A copy of `value`'s plain data, all the way down: each array and plain object (on
|
|
224
|
+
* the same prototype), with every own property, its non-enumerable and symbol-keyed
|
|
225
|
+
* ones included. `made` maps each original to its copy (so a cycle stays one).
|
|
226
|
+
* Anything else (a primitive, a class instance, a `Map`, a `Set`, a `Date`, a typed
|
|
227
|
+
* array, a function) is kept as the app gave it, and so is a getter.
|
|
228
|
+
*/
|
|
229
|
+
const plainCopy = (value, made) => {
|
|
230
|
+
if (typeof value !== "object" || value === null || !isPlainData(value)) {
|
|
231
|
+
return value;
|
|
232
|
+
}
|
|
233
|
+
const done = made.get(value);
|
|
234
|
+
if (done !== undefined) {
|
|
235
|
+
return done;
|
|
236
|
+
}
|
|
237
|
+
const copy = Array.isArray(value)
|
|
238
|
+
? []
|
|
239
|
+
: Object.create(Object.getPrototypeOf(value));
|
|
240
|
+
made.set(value, copy);
|
|
241
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
242
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
243
|
+
if (descriptor === undefined) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if ("value" in descriptor) {
|
|
247
|
+
descriptor.value = plainCopy(descriptor.value, made);
|
|
248
|
+
}
|
|
249
|
+
Object.defineProperty(copy, key, descriptor);
|
|
250
|
+
}
|
|
251
|
+
return copy;
|
|
252
|
+
};
|
|
253
|
+
/**
|
|
254
|
+
* Refuses a token for a session the store no longer has (revoked, signed out,
|
|
255
|
+
* expired): with Better Auth's cookie cache on, a session read from its cookie may
|
|
256
|
+
* have been revoked since, so a token is issued only for one the store still has.
|
|
257
|
+
*/
|
|
258
|
+
export const sessionStillLive = async (store, session, refuse) => {
|
|
259
|
+
const { token } = session;
|
|
260
|
+
const found = typeof token === "string"
|
|
261
|
+
? await (await store()).internalAdapter.findSession(token)
|
|
262
|
+
: null;
|
|
263
|
+
if (found === null || found.session.expiresAt.getTime() <= Date.now()) {
|
|
264
|
+
throw refuse();
|
|
265
|
+
}
|
|
266
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { type AuditLog, type SignInIdentifier } from "../audit/audit.log.js";
|
|
2
|
+
import type { BetterAuthModules } from "./better-auth.loader.js";
|
|
3
|
+
import { type AccessProfileKind } from "./profile.policy.js";
|
|
4
|
+
/** A session row, as Better Auth's database hooks hand it over. */
|
|
5
|
+
type HookSession = {
|
|
6
|
+
readonly userId?: unknown;
|
|
7
|
+
} | null | undefined;
|
|
8
|
+
/** The endpoint context Better Auth hands a hook's matcher and its database hooks. */
|
|
9
|
+
type HookContext = {
|
|
10
|
+
readonly request?: unknown;
|
|
11
|
+
readonly path?: unknown;
|
|
12
|
+
} | null | undefined;
|
|
13
|
+
/** Where Better Auth's context names its session cookies. */
|
|
14
|
+
type CookieContext = {
|
|
15
|
+
readonly authCookies?: {
|
|
16
|
+
readonly sessionToken?: {
|
|
17
|
+
readonly name?: unknown;
|
|
18
|
+
};
|
|
19
|
+
readonly sessionData?: {
|
|
20
|
+
readonly name?: unknown;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
/** The part of a response's headers the service profile's cookie strip reads and changes. */
|
|
25
|
+
type ResponseCookies = {
|
|
26
|
+
getSetCookie(): string[];
|
|
27
|
+
delete(name: string): void;
|
|
28
|
+
append(name: string, value: string): void;
|
|
29
|
+
};
|
|
30
|
+
/** The session cookies' bare names: the token's, and the cookie cache's, which carries the session too. */
|
|
31
|
+
export declare const sessionCookies: (context: CookieContext) => readonly string[];
|
|
32
|
+
/**
|
|
33
|
+
* The service profile's last word on what `access.handler` answers, after Better
|
|
34
|
+
* Auth's router and every plugin's `onResponse`: no session cookie (`names`,
|
|
35
|
+
* through `sessionCookies`), whatever set it. The headers are changed in place; a
|
|
36
|
+
* response whose headers cannot change is answered with a bare 404 instead.
|
|
37
|
+
*/
|
|
38
|
+
export declare const withoutSessionCookies: (response: {
|
|
39
|
+
readonly headers: ResponseCookies;
|
|
40
|
+
}, names: readonly string[]) => {
|
|
41
|
+
readonly headers: ResponseCookies;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* The identifier a sign-in names, read as Better Auth 1.7.12 to 1.7.14 reads it on that route
|
|
45
|
+
* and nowhere else (any other field of the body is the caller's to fill): an email
|
|
46
|
+
* lower-cased, as `/sign-in/email` and `/sign-in/email-otp` look it up; a username
|
|
47
|
+
* through the `username()` plugin's `usernameNormalization` (lower-cased by default,
|
|
48
|
+
* as given with `false`); a phone number as given, as `/sign-in/phone-number` looks
|
|
49
|
+
* it up.
|
|
50
|
+
*/
|
|
51
|
+
export declare const signInIdentifier: (plugins: readonly unknown[] | (() => readonly unknown[])) => SignInIdentifier;
|
|
52
|
+
/**
|
|
53
|
+
* The Access plugin's members for sessions (`init` with its database hooks, and
|
|
54
|
+
* `hooks`), for the profile and the audit log; none when neither needs them.
|
|
55
|
+
*/
|
|
56
|
+
export declare const sessionWatch: (input: {
|
|
57
|
+
readonly kind: AccessProfileKind;
|
|
58
|
+
readonly auditLog: AuditLog | undefined;
|
|
59
|
+
readonly modules: Pick<BetterAuthModules, "createAuthMiddleware" | "APIError">;
|
|
60
|
+
/** The app's Better Auth plugins (or, for the Access plugin, a read of them once Better Auth starts it): how they read a sign-in's identifier. */
|
|
61
|
+
readonly plugins: readonly unknown[] | (() => readonly unknown[]);
|
|
62
|
+
/** The kiosk profile's own step before a session is created (`kioskPlugin`'s `sessionCreating`): it binds one on an enrolled device. */
|
|
63
|
+
readonly sessionCreating?: (session: {
|
|
64
|
+
readonly kioskDeviceId?: unknown;
|
|
65
|
+
} | null | undefined, context: HookContext) => Promise<false | {
|
|
66
|
+
readonly data: object;
|
|
67
|
+
} | undefined>;
|
|
68
|
+
/** The kiosk profile's own step once a session is created (`kioskPlugin`'s `sessionCreated`). */
|
|
69
|
+
readonly sessionCreated?: (session: HookSession, context: HookContext) => Promise<void>;
|
|
70
|
+
}) => {
|
|
71
|
+
init?: never;
|
|
72
|
+
hooks?: never;
|
|
73
|
+
} | {
|
|
74
|
+
init: () => {
|
|
75
|
+
options: {
|
|
76
|
+
databaseHooks: {
|
|
77
|
+
session: {
|
|
78
|
+
delete?: {
|
|
79
|
+
after: (session: {
|
|
80
|
+
readonly userId?: unknown;
|
|
81
|
+
readonly impersonatedBy?: unknown;
|
|
82
|
+
} | null | undefined, context: {
|
|
83
|
+
readonly request?: unknown;
|
|
84
|
+
readonly path?: unknown;
|
|
85
|
+
readonly body?: unknown;
|
|
86
|
+
readonly context?: {
|
|
87
|
+
readonly secret?: unknown;
|
|
88
|
+
} | undefined;
|
|
89
|
+
} | null | undefined) => Promise<void>;
|
|
90
|
+
};
|
|
91
|
+
create: {
|
|
92
|
+
after?: (session: HookSession, context: HookContext) => Promise<void>;
|
|
93
|
+
before?: (session: {
|
|
94
|
+
readonly kioskDeviceId?: unknown;
|
|
95
|
+
} | null | undefined, context: HookContext) => Promise<false | {
|
|
96
|
+
readonly data: object;
|
|
97
|
+
} | undefined>;
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
hooks: {
|
|
104
|
+
before: {
|
|
105
|
+
matcher: (ctx: HookContext) => boolean;
|
|
106
|
+
handler: unknown;
|
|
107
|
+
}[];
|
|
108
|
+
after: {
|
|
109
|
+
matcher: (ctx: HookContext) => boolean;
|
|
110
|
+
handler: unknown;
|
|
111
|
+
}[];
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
export {};
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
// The Better Auth hooks the Access plugin installs over sessions: the service
|
|
2
|
+
// profile's refusals, and the audit log's events from Better Auth's request path.
|
|
3
|
+
//
|
|
4
|
+
// - Service profile: no request through `access.handler` gets a session. A route
|
|
5
|
+
// that starts one (`startsSession`, by Better Auth's own endpoint path, `ctx.path`)
|
|
6
|
+
// is refused with a 404 before it runs. Any other route (a plugin's the list does
|
|
7
|
+
// not know) cannot create one either: Better Auth's `session.create` before-hook
|
|
8
|
+
// refuses the row, so no session exists even for a moment, and once the endpoint
|
|
9
|
+
// has answered, the answer is replaced by a bare 404, its cookies and headers
|
|
10
|
+
// dropped. So is an answer that sets the session cookie of a session that already
|
|
11
|
+
// existed. And `access.handler` itself strips any session cookie from what Better
|
|
12
|
+
// Auth answers (`withoutSessionCookies`), after every plugin's `onResponse`, which
|
|
13
|
+
// also covers a route that returns its own `Response` (Better Auth runs no after
|
|
14
|
+
// hook on one). A route that puts a session token in a JSON body is
|
|
15
|
+
// not covered: no built-in plugin does. The server's own calls to Better Auth's
|
|
16
|
+
// API with no request sign users in as before.
|
|
17
|
+
// - Audit: sign-ins and sign-outs from Better Auth's session hooks, refused sign-ins
|
|
18
|
+
// and issued tokens from its endpoint hooks (`audit.log.ts` says what each event
|
|
19
|
+
// holds).
|
|
20
|
+
import { endpointAudit, sessionAuditHooks, } from "../audit/audit.log.js";
|
|
21
|
+
import { KIOSK_SWITCH_PATH } from "../kiosk/kiosk-verbs.schema.js";
|
|
22
|
+
import { servesRoute, startsSession, } from "./profile.policy.js";
|
|
23
|
+
/** Whether the call came through `access.handler`: a request, not the server's own API call. */
|
|
24
|
+
const fromOutside = (context) => typeof context?.request === "object" && context.request !== null;
|
|
25
|
+
/** A cookie name as a browser could match it to Better Auth's: no case, no `__Secure-`/`__Host-` prefix. */
|
|
26
|
+
const bareCookie = (name) => name
|
|
27
|
+
.trim()
|
|
28
|
+
.toLowerCase()
|
|
29
|
+
.replace(/^__(?:secure|host)-/, "");
|
|
30
|
+
/** The session cookies' bare names: the token's, and the cookie cache's, which carries the session too. */
|
|
31
|
+
export const sessionCookies = (context) => {
|
|
32
|
+
const names = [];
|
|
33
|
+
for (const name of [
|
|
34
|
+
context.authCookies?.sessionToken?.name,
|
|
35
|
+
context.authCookies?.sessionData?.name,
|
|
36
|
+
]) {
|
|
37
|
+
if (typeof name === "string") {
|
|
38
|
+
names.push(bareCookie(name));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return names;
|
|
42
|
+
};
|
|
43
|
+
/** The bare name a `Set-Cookie` line sets, and whether it sets a value (not one that deletes it). */
|
|
44
|
+
const cookieSet = (line) => {
|
|
45
|
+
const [pair = ""] = line.split(";");
|
|
46
|
+
const equals = pair.indexOf("=");
|
|
47
|
+
return {
|
|
48
|
+
name: bareCookie(equals < 0 ? pair : pair.slice(0, equals)),
|
|
49
|
+
value: equals >= 0 && pair.slice(equals + 1).trim() !== "",
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Whether a cookie's bare name is a session cookie's: one of `names`, or the
|
|
54
|
+
* `multiSession()` plugin's `<session_token>_multi-<token>`.
|
|
55
|
+
*/
|
|
56
|
+
const isSessionCookie = (name, names) => {
|
|
57
|
+
for (const session of names) {
|
|
58
|
+
if (name === session || name.startsWith(`${session}_multi-`)) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
};
|
|
64
|
+
/** Whether response headers set one of `names` to a value. */
|
|
65
|
+
const setsCookie = (headers, names) => {
|
|
66
|
+
for (const line of headers?.getSetCookie() ?? []) {
|
|
67
|
+
const set = cookieSet(line);
|
|
68
|
+
if (set.value && isSessionCookie(set.name, names)) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* The service profile's last word on what `access.handler` answers, after Better
|
|
76
|
+
* Auth's router and every plugin's `onResponse`: no session cookie (`names`,
|
|
77
|
+
* through `sessionCookies`), whatever set it. The headers are changed in place; a
|
|
78
|
+
* response whose headers cannot change is answered with a bare 404 instead.
|
|
79
|
+
*/
|
|
80
|
+
export const withoutSessionCookies = (response, names) => {
|
|
81
|
+
const lines = response.headers.getSetCookie();
|
|
82
|
+
const kept = [];
|
|
83
|
+
for (const line of lines) {
|
|
84
|
+
if (!isSessionCookie(cookieSet(line).name, names)) {
|
|
85
|
+
kept.push(line);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (kept.length === lines.length) {
|
|
89
|
+
return response;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
response.headers.delete("set-cookie");
|
|
93
|
+
for (const line of kept) {
|
|
94
|
+
response.headers.append("set-cookie", line);
|
|
95
|
+
}
|
|
96
|
+
return response;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return new Response(null, { status: 404 });
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
/** The sign-in routes that name their user, by the body field each reads it from. */
|
|
103
|
+
const IDENTIFIED = {
|
|
104
|
+
"/sign-in/email": "email",
|
|
105
|
+
"/sign-in/email-otp": "email",
|
|
106
|
+
"/sign-in/username": "username",
|
|
107
|
+
"/sign-in/phone-number": "phoneNumber",
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* The identifier a sign-in names, read as Better Auth 1.7.12 to 1.7.14 reads it on that route
|
|
111
|
+
* and nowhere else (any other field of the body is the caller's to fill): an email
|
|
112
|
+
* lower-cased, as `/sign-in/email` and `/sign-in/email-otp` look it up; a username
|
|
113
|
+
* through the `username()` plugin's `usernameNormalization` (lower-cased by default,
|
|
114
|
+
* as given with `false`); a phone number as given, as `/sign-in/phone-number` looks
|
|
115
|
+
* it up.
|
|
116
|
+
*/
|
|
117
|
+
export const signInIdentifier = (plugins) => {
|
|
118
|
+
let normalization;
|
|
119
|
+
let read = false;
|
|
120
|
+
/** The `username()` plugin's normalization, read once: the plugins known now, or (the Access plugin) when Better Auth starts it. */
|
|
121
|
+
const normalizationOf = () => {
|
|
122
|
+
if (!read) {
|
|
123
|
+
read = true;
|
|
124
|
+
for (const plugin of typeof plugins === "function"
|
|
125
|
+
? plugins()
|
|
126
|
+
: plugins) {
|
|
127
|
+
const { id, options } = (plugin ?? {});
|
|
128
|
+
if (id === "username") {
|
|
129
|
+
normalization = options?.usernameNormalization;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return normalization;
|
|
134
|
+
};
|
|
135
|
+
if (typeof plugins !== "function") {
|
|
136
|
+
normalizationOf();
|
|
137
|
+
}
|
|
138
|
+
return (path, body) => {
|
|
139
|
+
const field = typeof path === "string" ? IDENTIFIED[path] : undefined;
|
|
140
|
+
const value = field === undefined
|
|
141
|
+
? undefined
|
|
142
|
+
: body?.[field];
|
|
143
|
+
if (field === undefined || typeof value !== "string") {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
if (field !== "username") {
|
|
147
|
+
return `${field}:${field === "email" ? value.toLowerCase() : value}`;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
const normalized = normalizationOf() === false
|
|
151
|
+
? value
|
|
152
|
+
: typeof normalizationOf() === "function"
|
|
153
|
+
? normalizationOf()(value)
|
|
154
|
+
: value.toLowerCase();
|
|
155
|
+
return typeof normalized === "string"
|
|
156
|
+
? `username:${normalized}`
|
|
157
|
+
: undefined;
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
/**
|
|
165
|
+
* The Access plugin's members for sessions (`init` with its database hooks, and
|
|
166
|
+
* `hooks`), for the profile and the audit log; none when neither needs them.
|
|
167
|
+
*/
|
|
168
|
+
export const sessionWatch = (input) => {
|
|
169
|
+
const { kind, auditLog, modules, sessionCreating, sessionCreated } = input;
|
|
170
|
+
const service = kind === "service";
|
|
171
|
+
if (!service &&
|
|
172
|
+
auditLog === undefined &&
|
|
173
|
+
sessionCreated === undefined &&
|
|
174
|
+
sessionCreating === undefined) {
|
|
175
|
+
return {};
|
|
176
|
+
}
|
|
177
|
+
const identify = signInIdentifier(input.plugins);
|
|
178
|
+
/** The requests through the handler whose session the service profile refused. */
|
|
179
|
+
const refused = new WeakSet();
|
|
180
|
+
const audit = auditLog === undefined ? undefined : sessionAuditHooks(auditLog, identify);
|
|
181
|
+
const created = audit === undefined && sessionCreated === undefined
|
|
182
|
+
? undefined
|
|
183
|
+
: async (session, context) => {
|
|
184
|
+
await audit?.session.create.after(session, context);
|
|
185
|
+
await sessionCreated?.(session, context);
|
|
186
|
+
};
|
|
187
|
+
const databaseHooks = {
|
|
188
|
+
session: {
|
|
189
|
+
create: {
|
|
190
|
+
...(service
|
|
191
|
+
? {
|
|
192
|
+
before: async (_session, context) => {
|
|
193
|
+
if (!fromOutside(context)) {
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
refused.add(context.request);
|
|
197
|
+
return false;
|
|
198
|
+
},
|
|
199
|
+
}
|
|
200
|
+
: sessionCreating === undefined
|
|
201
|
+
? {}
|
|
202
|
+
: { before: sessionCreating }),
|
|
203
|
+
...(created === undefined ? {} : { after: created }),
|
|
204
|
+
},
|
|
205
|
+
...(audit === undefined ? {} : { delete: audit.session.delete }),
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
const notFound = () => new modules.APIError("NOT_FOUND");
|
|
209
|
+
const before = service
|
|
210
|
+
? [
|
|
211
|
+
{
|
|
212
|
+
matcher: (ctx) => fromOutside(ctx) && !servesRoute(kind, ctx.path),
|
|
213
|
+
handler: modules.createAuthMiddleware(async () => {
|
|
214
|
+
throw notFound();
|
|
215
|
+
}),
|
|
216
|
+
},
|
|
217
|
+
]
|
|
218
|
+
: [];
|
|
219
|
+
const after = [
|
|
220
|
+
...(auditLog === undefined
|
|
221
|
+
? []
|
|
222
|
+
: [
|
|
223
|
+
{
|
|
224
|
+
// The kiosk's PIN switch logs its own refusals, with their device.
|
|
225
|
+
matcher: (ctx) => (startsSession(ctx?.path) && ctx?.path !== KIOSK_SWITCH_PATH) ||
|
|
226
|
+
ctx?.path === "/token",
|
|
227
|
+
handler: modules.createAuthMiddleware(endpointAudit(auditLog, startsSession, identify)),
|
|
228
|
+
},
|
|
229
|
+
]),
|
|
230
|
+
...(service
|
|
231
|
+
? [
|
|
232
|
+
{
|
|
233
|
+
matcher: fromOutside,
|
|
234
|
+
handler: modules.createAuthMiddleware(async ({ request, context }) => {
|
|
235
|
+
const headers = context.responseHeaders;
|
|
236
|
+
const handsOutSession = (typeof request === "object" &&
|
|
237
|
+
request !== null &&
|
|
238
|
+
refused.has(request)) ||
|
|
239
|
+
setsCookie(headers, sessionCookies(context));
|
|
240
|
+
if (!handsOutSession) {
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
for (const name of [...(headers?.keys() ?? [])]) {
|
|
244
|
+
headers?.delete(name);
|
|
245
|
+
}
|
|
246
|
+
// A Response, not an error: Better Auth keeps the status an
|
|
247
|
+
// endpoint refused with over an after hook's error.
|
|
248
|
+
return new Response(null, { status: 404 });
|
|
249
|
+
}),
|
|
250
|
+
},
|
|
251
|
+
]
|
|
252
|
+
: []),
|
|
253
|
+
];
|
|
254
|
+
return {
|
|
255
|
+
init: () => ({ options: { databaseHooks } }),
|
|
256
|
+
hooks: { before, after },
|
|
257
|
+
};
|
|
258
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type JSONWebKeySet } from "jose";
|
|
2
|
+
import { type AccessClaimsDeclaration } from "./claims.mapper.js";
|
|
3
|
+
/** Where the key set comes from, and what a token must state. */
|
|
4
|
+
type TokenVerifierSource = {
|
|
5
|
+
/** The published key set: `{ keys: [...] }`. */
|
|
6
|
+
readonly keys: () => Promise<JSONWebKeySet>;
|
|
7
|
+
readonly claims: AccessClaimsDeclaration;
|
|
8
|
+
/** The `iss` and the `aud` every token carries: the instance's fixed issuer. */
|
|
9
|
+
readonly issuer: string;
|
|
10
|
+
/** How long a token lives, in seconds: one issued longer ago is refused. */
|
|
11
|
+
readonly tokenTtl: number;
|
|
12
|
+
/** Milliseconds since the epoch; a test passes its own clock. */
|
|
13
|
+
readonly now?: () => number;
|
|
14
|
+
};
|
|
15
|
+
export type TokenVerifier = {
|
|
16
|
+
/** The identity a token proves, or `null` for a token that is not valid, current and Access's own. */
|
|
17
|
+
verify(token: string): Promise<{
|
|
18
|
+
readonly sub: string;
|
|
19
|
+
readonly claims: {
|
|
20
|
+
readonly [claim: string]: string | number | boolean;
|
|
21
|
+
};
|
|
22
|
+
} | null>;
|
|
23
|
+
};
|
|
24
|
+
/** The clock skew allowed between the issuer and the verifier, in seconds. */
|
|
25
|
+
export declare const CLOCK_TOLERANCE_S = 5;
|
|
26
|
+
/**
|
|
27
|
+
* The algorithms a token may be signed with: those Better Auth's `jwt()` signs with
|
|
28
|
+
* (its `keyPairConfig.alg`), all asymmetric. A token signed otherwise is refused
|
|
29
|
+
* before its key is looked up, whatever key the set holds.
|
|
30
|
+
*/
|
|
31
|
+
export declare const JWT_ALGORITHMS: readonly ["EdDSA", "ES256", "ES512", "PS256", "RS256"];
|
|
32
|
+
/** Whether a JWT header's `typ` names an OAuth access token (RFC 9068, section 2.1: `at+jwt`, or `application/at+jwt`). */
|
|
33
|
+
export declare const isAccessTokenType: (typ: unknown) => boolean;
|
|
34
|
+
export declare const createTokenVerifier: (source: TokenVerifierSource) => TokenVerifier;
|
|
35
|
+
export {};
|