@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,40 @@
|
|
|
1
|
+
// `avaccess verify`: the pulled files on disk against `avAccess.lock.json`, offline.
|
|
2
|
+
// It reads the lock and the files it pins and runs the engine's own check
|
|
3
|
+
// (`lock.verifier.ts`): no registry, no network. It belongs at the front of a
|
|
4
|
+
// project's `aventara:prepare` script, so a pulled file edited by hand stops the
|
|
5
|
+
// build before anything is generated from it.
|
|
6
|
+
import { foreignOwnersOf, verifyLocked } from "../lock/lock.verifier.js";
|
|
7
|
+
import { LOCK_FILE } from "../lock/lock-contract.js";
|
|
8
|
+
import { environmentOf, verifyBeaconPins } from "./beacon-pins.file.js";
|
|
9
|
+
import { CliFailure, codeOf, EXIT_FAILED, EXIT_OK, messageOf, positionals, } from "./cli.context.js";
|
|
10
|
+
import { readForeignPinned, readLock, readPinned } from "./lock.file.js";
|
|
11
|
+
export const verifyCommand = async (context) => {
|
|
12
|
+
positionals(context, "avaccess verify [--env <environment>]", 0, 0);
|
|
13
|
+
const lock = await readLock(context.cwd);
|
|
14
|
+
const policies = await readPinned(context.cwd, lock.policies);
|
|
15
|
+
const guardrails = lock.guardrails === null
|
|
16
|
+
? undefined
|
|
17
|
+
: await readPinned(context.cwd, lock.guardrails);
|
|
18
|
+
try {
|
|
19
|
+
verifyLocked(lock, {
|
|
20
|
+
namespace: policies
|
|
21
|
+
?.namespace,
|
|
22
|
+
policies,
|
|
23
|
+
guardrails,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
throw new CliFailure(EXIT_FAILED, messageOf(error), codeOf(error));
|
|
28
|
+
}
|
|
29
|
+
await readForeignPinned(context.cwd, lock);
|
|
30
|
+
const owners = foreignOwnersOf(policies);
|
|
31
|
+
const environments = await verifyBeaconPins(context.cwd, lock.namespace, owners, environmentOf(context));
|
|
32
|
+
context.out(`verified against ${LOCK_FILE}: ${lock.policies.path} (${lock.policies.hash})${lock.guardrails === null
|
|
33
|
+
? ", and no guardrails pinned"
|
|
34
|
+
: ` and ${lock.guardrails.path} (${lock.guardrails.hash})`}${lock.foreign === undefined
|
|
35
|
+
? ""
|
|
36
|
+
: `, and the foreign facts ${lock.foreign.path} (${lock.foreign.hash})`}${environments.length === 0
|
|
37
|
+
? ""
|
|
38
|
+
: `; the beacon pins of ${environments.map((environment) => `"${environment}"`).join(", ")} cover what the rules read`}`);
|
|
39
|
+
return EXIT_OK;
|
|
40
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { AccessClaimsDeclaration } from "../access/claims.mapper.js";
|
|
2
|
+
import { type AccessProfileKind } from "../access/profile.policy.js";
|
|
3
|
+
import type { SignInIdentifier } from "../audit/audit.log.js";
|
|
4
|
+
import { type AccessKioskVerbName } from "../kiosk/kiosk-verbs.schema.js";
|
|
5
|
+
import { type AccessVerbSchema } from "./access-verbs.schema.js";
|
|
6
|
+
import type { LimitedCall, LimitSettle } from "./verb-rate.limiter.js";
|
|
7
|
+
/** The headers of a request, as the framework hands them to `call`. */
|
|
8
|
+
type RequestHeaders = {
|
|
9
|
+
get(name: string): string | null;
|
|
10
|
+
has(name: string): boolean;
|
|
11
|
+
forEach(callback: (value: string, name: string) => void): void;
|
|
12
|
+
};
|
|
13
|
+
/** One verb as `access.client` publishes it: its route, its shapes and its server call. */
|
|
14
|
+
type AccessClientVerb = {
|
|
15
|
+
readonly method: "GET" | "POST";
|
|
16
|
+
readonly path: string;
|
|
17
|
+
readonly args?: AccessVerbSchema;
|
|
18
|
+
readonly result: AccessVerbSchema;
|
|
19
|
+
call(input: {
|
|
20
|
+
readonly args: unknown;
|
|
21
|
+
readonly headers: RequestHeaders;
|
|
22
|
+
readonly request?: unknown;
|
|
23
|
+
}): Promise<unknown>;
|
|
24
|
+
};
|
|
25
|
+
/** The verbs `access.client` publishes: each one the instance serves. */
|
|
26
|
+
type AccessClientVerbs = {
|
|
27
|
+
signIn?: AccessClientVerb;
|
|
28
|
+
signUp?: AccessClientVerb;
|
|
29
|
+
signOut?: AccessClientVerb;
|
|
30
|
+
resetPassword?: AccessClientVerb;
|
|
31
|
+
me?: AccessClientVerb;
|
|
32
|
+
} & {
|
|
33
|
+
[V in AccessKioskVerbName]?: AccessClientVerb;
|
|
34
|
+
};
|
|
35
|
+
/** `access.client`. */
|
|
36
|
+
type AccessClient = {
|
|
37
|
+
readonly url: string;
|
|
38
|
+
readonly verbs: Readonly<AccessClientVerbs>;
|
|
39
|
+
};
|
|
40
|
+
/** Better Auth's refusal, as `call` throws one: a status, and a sentence. */
|
|
41
|
+
/** A refusal's body: Better Auth's `{ message, code }`. */
|
|
42
|
+
type RefusalBody = {
|
|
43
|
+
readonly message?: unknown;
|
|
44
|
+
readonly code?: unknown;
|
|
45
|
+
};
|
|
46
|
+
type RefusalError = new (status: string | number, body: RefusalBody, headers?: {
|
|
47
|
+
readonly [name: string]: string;
|
|
48
|
+
}) => Error;
|
|
49
|
+
/**
|
|
50
|
+
* Builds `access.client` for an instance: from its Better Auth options (`baseURL`,
|
|
51
|
+
* `basePath`, `emailAndPassword`, `disabledPaths`), its profile and its declared
|
|
52
|
+
* claims.
|
|
53
|
+
*/
|
|
54
|
+
export declare const accessClient: (input: {
|
|
55
|
+
readonly block: {
|
|
56
|
+
readonly [key: string]: unknown;
|
|
57
|
+
};
|
|
58
|
+
readonly kind: AccessProfileKind;
|
|
59
|
+
readonly claims: AccessClaimsDeclaration;
|
|
60
|
+
/** Better Auth's base URL resolution, as `getBaseURL` resolves it. */
|
|
61
|
+
readonly baseOf: (baseURL: string | undefined, basePath: string | undefined) => string | undefined;
|
|
62
|
+
readonly isDynamicBaseURL: (baseURL: unknown) => boolean;
|
|
63
|
+
/** Better Auth's server API, built on first use. */
|
|
64
|
+
readonly api: () => unknown;
|
|
65
|
+
/** The request's identity, as `access.state` reads it. */
|
|
66
|
+
readonly identityOf: (headers: RequestHeaders, request: unknown) => Promise<unknown>;
|
|
67
|
+
/** Where `call` registers the requests it builds, for the Access plugin's origin check. */
|
|
68
|
+
readonly calls: WeakSet<object>;
|
|
69
|
+
readonly refusal: RefusalError;
|
|
70
|
+
/** How the sign-in route reads its identifier from a body (`email:<address>`). */
|
|
71
|
+
readonly identify: SignInIdentifier;
|
|
72
|
+
/** The rate limit on `signIn`, `signUp` and `resetPassword`: throws Better Auth's 429, else answers what clears the target's count once the call succeeds. */
|
|
73
|
+
readonly limit: (call: LimitedCall) => Promise<LimitSettle | undefined>;
|
|
74
|
+
/** Better Auth's context, once Better Auth is built: its plugins' `onRequest` hooks run with it. */
|
|
75
|
+
readonly context: () => Promise<unknown>;
|
|
76
|
+
}) => AccessClient;
|
|
77
|
+
/** Better Auth's `createAuthEndpoint`, as Access's `me` route calls it. */
|
|
78
|
+
export type MeEndpointFactory = (path: string, options: {
|
|
79
|
+
readonly method: "GET";
|
|
80
|
+
}, handler: (context: {
|
|
81
|
+
readonly headers?: Headers | undefined;
|
|
82
|
+
readonly request?: unknown;
|
|
83
|
+
}) => Promise<Response>) => unknown;
|
|
84
|
+
/**
|
|
85
|
+
* Access's `GET /me` route (under Better Auth's base path), keyed as Better Auth keys
|
|
86
|
+
* a plugin's endpoints (`access.api.accessMe`): the request's identity, `{ sub,
|
|
87
|
+
* claims }`, or `null`, as `access.client.verbs.me.call` answers it. Never cached.
|
|
88
|
+
*/
|
|
89
|
+
export declare const meEndpoint: (endpoint: MeEndpointFactory, identityOf: (headers: RequestHeaders, request: unknown) => Promise<unknown>) => {
|
|
90
|
+
accessMe: unknown;
|
|
91
|
+
};
|
|
92
|
+
export {};
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
// `access.client`: what the framework's `$access` reads from an Access instance,
|
|
2
|
+
// through the provider member `AvAccessProvider.client`. Where Access is mounted (`url`,
|
|
3
|
+
// from Better Auth's `baseURL` and `basePath`), and for each verb the instance
|
|
4
|
+
// serves, its route, its shapes and its server call.
|
|
5
|
+
//
|
|
6
|
+
// A verb is offered only where its route answers: `signIn`, `signUp` and
|
|
7
|
+
// `resetPassword` with Better Auth's `emailAndPassword` on (`signUp` not with
|
|
8
|
+
// `disableSignUp`), no verb whose path `disabledPaths` lists, and on the `service`
|
|
9
|
+
// profile no verb that starts a session (`signIn`, `signUp`), by the same rule its
|
|
10
|
+
// hooks refuse those routes with.
|
|
11
|
+
//
|
|
12
|
+
// On the `kiosk` profile, `verbs` holds the kiosk's verbs too (`switchUser`,
|
|
13
|
+
// `lock`, `setPin`, `enrollDevice`, `revokeDevice`), in the same form: the
|
|
14
|
+
// framework's `$access` takes them as known optional verbs. A manager's device list
|
|
15
|
+
// (`GET /kiosk/list-devices`, and `access.kiosk` on the server) is no verb: the
|
|
16
|
+
// framework's set of verbs is closed.
|
|
17
|
+
//
|
|
18
|
+
// `call` is the server's way in (a Server Action, a Nest controller): it runs the
|
|
19
|
+
// verb in-process through Better Auth's API, so a cookie plugin such as Better
|
|
20
|
+
// Auth's `nextCookies()` sets the browser's cookie from the answer. It runs the
|
|
21
|
+
// verb as the request it stands for: Better Auth gets a request built from the
|
|
22
|
+
// caller's headers, so everything Better Auth and Access hold a request to holds
|
|
23
|
+
// here too (the `service` profile's refusals, the endpoint's own CSRF check, the
|
|
24
|
+
// audit log's `client` origin), and the Access plugin runs Better Auth's own origin
|
|
25
|
+
// check on it, the one its router runs before every route (`originCheckMiddleware`:
|
|
26
|
+
// the `Origin` of a request with a cookie, and every `callbackURL` and `redirectTo`
|
|
27
|
+
// against `trustedOrigins`). Its arguments are held to the verb's schema, and its
|
|
28
|
+
// answer is cut to the verb's result schema: no session token and no user field
|
|
29
|
+
// leaves it. `me` reads the request's identity as `access.state` does, from its
|
|
30
|
+
// bearer token or (profile `web`) its session cookie, and answers it, `{ sub,
|
|
31
|
+
// claims }`, or `null`.
|
|
32
|
+
import { servesRoute, } from "../access/profile.policy.js";
|
|
33
|
+
import { KIOSK_VERBS, } from "../kiosk/kiosk-verbs.schema.js";
|
|
34
|
+
import { BETTER_AUTH_VERBS, ME_PATH, meSchema, projected, schemaDefect, } from "./access-verbs.schema.js";
|
|
35
|
+
/** Better Auth's names for the statuses a plugin refuses with; another status is thrown as its number. */
|
|
36
|
+
const STATUS_NAMES = {
|
|
37
|
+
400: "BAD_REQUEST",
|
|
38
|
+
401: "UNAUTHORIZED",
|
|
39
|
+
403: "FORBIDDEN",
|
|
40
|
+
404: "NOT_FOUND",
|
|
41
|
+
429: "TOO_MANY_REQUESTS",
|
|
42
|
+
500: "INTERNAL_SERVER_ERROR",
|
|
43
|
+
};
|
|
44
|
+
const NO_ARGS = {
|
|
45
|
+
type: "object",
|
|
46
|
+
properties: {},
|
|
47
|
+
additionalProperties: false,
|
|
48
|
+
};
|
|
49
|
+
const headersOf = (given) => {
|
|
50
|
+
const headers = new Headers();
|
|
51
|
+
given.forEach((value, name) => {
|
|
52
|
+
headers.append(name, value);
|
|
53
|
+
});
|
|
54
|
+
return headers;
|
|
55
|
+
};
|
|
56
|
+
/** The verbs on Better Auth's routes, in the order `access.client` lists them. */
|
|
57
|
+
const BETTER_AUTH_VERB_NAMES = [
|
|
58
|
+
"signIn",
|
|
59
|
+
"signUp",
|
|
60
|
+
"signOut",
|
|
61
|
+
"resetPassword",
|
|
62
|
+
];
|
|
63
|
+
/** Freezes a verb, the verbs or the client: published data, not to be changed. */
|
|
64
|
+
const freeze = Object.freeze;
|
|
65
|
+
/** A path without its trailing slashes, `/` kept as `/`. */
|
|
66
|
+
const trimmed = (path) => path.replace(/\/+$/, "") || "/";
|
|
67
|
+
/**
|
|
68
|
+
* Builds `access.client` for an instance: from its Better Auth options (`baseURL`,
|
|
69
|
+
* `basePath`, `emailAndPassword`, `disabledPaths`), its profile and its declared
|
|
70
|
+
* claims.
|
|
71
|
+
*/
|
|
72
|
+
export const accessClient = (input) => {
|
|
73
|
+
const { block, kind } = input;
|
|
74
|
+
const basePath = typeof block.basePath === "string" ? block.basePath : undefined;
|
|
75
|
+
const dynamic = input.isDynamicBaseURL(block.baseURL);
|
|
76
|
+
// A dynamic base URL follows the request's host: the client reaches Access on its
|
|
77
|
+
// own origin, at the base path. A fixed one (or Better Auth's environment
|
|
78
|
+
// variables) is where Access is.
|
|
79
|
+
const absolute = dynamic
|
|
80
|
+
? input.baseOf(block.baseURL.fallback, basePath)
|
|
81
|
+
: input.baseOf(typeof block.baseURL === "string" ? block.baseURL : undefined, basePath);
|
|
82
|
+
if (absolute === undefined) {
|
|
83
|
+
// `createAccess` refuses an instance without a base URL before it gets here.
|
|
84
|
+
throw new TypeError("access.client: Better Auth has no base URL");
|
|
85
|
+
}
|
|
86
|
+
const base = trimmed(absolute);
|
|
87
|
+
const url = dynamic ? trimmed(new URL(absolute).pathname) : base;
|
|
88
|
+
const emailAndPassword = (block.emailAndPassword ?? {});
|
|
89
|
+
const disabled = Array.isArray(block.disabledPaths)
|
|
90
|
+
? block.disabledPaths
|
|
91
|
+
: [];
|
|
92
|
+
const passwords = emailAndPassword.enabled === true;
|
|
93
|
+
const offered = {
|
|
94
|
+
signIn: passwords,
|
|
95
|
+
signUp: passwords && emailAndPassword.disableSignUp !== true,
|
|
96
|
+
signOut: true,
|
|
97
|
+
resetPassword: passwords,
|
|
98
|
+
};
|
|
99
|
+
const argsChecked = (verb, schema, args) => {
|
|
100
|
+
const defect = schemaDefect(schema ?? NO_ARGS, args ?? {}, "args");
|
|
101
|
+
if (defect !== undefined) {
|
|
102
|
+
throw new input.refusal("BAD_REQUEST", {
|
|
103
|
+
message: `$access.${verb}: ${defect}.`,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* The identifier a call names, for its rate limit's target: the email, read as
|
|
109
|
+
* the sign-in route reads it (sign-up's too, so one address is one target), or a
|
|
110
|
+
* password reset's token.
|
|
111
|
+
*/
|
|
112
|
+
const identifierOf = (verb, args) => {
|
|
113
|
+
if (verb !== "resetPassword") {
|
|
114
|
+
return input.identify("/sign-in/email", args);
|
|
115
|
+
}
|
|
116
|
+
const { token } = args;
|
|
117
|
+
return `reset-token:${token}`;
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* Every plugin's `onRequest`, in the order Better Auth's router runs them before
|
|
121
|
+
* a route (a server call skips them): a refusal is thrown as Better Auth's
|
|
122
|
+
* `APIError` with the plugin's status and body; a new request takes the place of
|
|
123
|
+
* the old one. Answers the request the route runs with.
|
|
124
|
+
*/
|
|
125
|
+
const pluginsAdmit = async (request) => {
|
|
126
|
+
const context = await input.context();
|
|
127
|
+
const plugins = context.options.plugins;
|
|
128
|
+
let current = request;
|
|
129
|
+
for (let at = 0; plugins !== undefined && at < plugins.length; at += 1) {
|
|
130
|
+
const onRequest = plugins[at]?.onRequest;
|
|
131
|
+
if (onRequest === undefined) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const answer = ((await onRequest(current, context)) ?? {});
|
|
135
|
+
const refused = answer.response;
|
|
136
|
+
if (refused !== undefined) {
|
|
137
|
+
let body;
|
|
138
|
+
try {
|
|
139
|
+
body = (await refused.clone().json());
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
body = { message: await refused.text() };
|
|
143
|
+
}
|
|
144
|
+
const headers = {};
|
|
145
|
+
refused.headers.forEach((value, name) => {
|
|
146
|
+
headers[name] = value;
|
|
147
|
+
});
|
|
148
|
+
throw new input.refusal(STATUS_NAMES[refused.status] ?? refused.status, body, headers);
|
|
149
|
+
}
|
|
150
|
+
if (answer.request !== undefined) {
|
|
151
|
+
current = answer.request;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return current;
|
|
155
|
+
};
|
|
156
|
+
/**
|
|
157
|
+
* One verb on a route of Better Auth's router (Better Auth's own or Access's), as
|
|
158
|
+
* published: its route, its shapes and its `call`. `limited` puts the call under
|
|
159
|
+
* the rate limit first (`signOut` and `lock` are not), `identifier` names its
|
|
160
|
+
* target for the limit's second bucket.
|
|
161
|
+
*/
|
|
162
|
+
const routed = (verb, route, limited, identifier) => {
|
|
163
|
+
const published = {
|
|
164
|
+
method: route.method,
|
|
165
|
+
path: route.path,
|
|
166
|
+
result: route.result,
|
|
167
|
+
call: async ({ args, headers, }) => {
|
|
168
|
+
argsChecked(verb, route.args, args);
|
|
169
|
+
const forwarded = headersOf(headers);
|
|
170
|
+
// The request this call stands for, as the route would get it: the
|
|
171
|
+
// caller's headers and the arguments as its JSON body.
|
|
172
|
+
forwarded.delete("content-length");
|
|
173
|
+
forwarded.delete("transfer-encoding");
|
|
174
|
+
if (args !== undefined) {
|
|
175
|
+
forwarded.set("content-type", "application/json");
|
|
176
|
+
}
|
|
177
|
+
const built = new Request(`${base}${route.path}`, {
|
|
178
|
+
method: route.method,
|
|
179
|
+
headers: forwarded,
|
|
180
|
+
body: args === undefined ? null : JSON.stringify(args),
|
|
181
|
+
});
|
|
182
|
+
// In the router's order: its rate limit, then the plugins' `onRequest`.
|
|
183
|
+
const settle = limited
|
|
184
|
+
? await input.limit({
|
|
185
|
+
path: route.path,
|
|
186
|
+
headers: forwarded,
|
|
187
|
+
request: built,
|
|
188
|
+
identifier: identifier(args),
|
|
189
|
+
})
|
|
190
|
+
: undefined;
|
|
191
|
+
const request = await pluginsAdmit(built);
|
|
192
|
+
let body = args;
|
|
193
|
+
if (request !== built && args !== undefined) {
|
|
194
|
+
// A plugin's new request: its body is what the route reads.
|
|
195
|
+
body = await request.clone().json();
|
|
196
|
+
argsChecked(verb, route.args, body);
|
|
197
|
+
}
|
|
198
|
+
input.calls.add(request);
|
|
199
|
+
// A refusal keeps the target's count; a success clears it.
|
|
200
|
+
const answer = await input.api()[route.endpoint]({
|
|
201
|
+
body,
|
|
202
|
+
headers: request.headers,
|
|
203
|
+
request,
|
|
204
|
+
asResponse: false,
|
|
205
|
+
});
|
|
206
|
+
await settle?.();
|
|
207
|
+
return projected(route.result, answer);
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
if (route.args !== undefined) {
|
|
211
|
+
published.args = route.args;
|
|
212
|
+
}
|
|
213
|
+
freeze(published);
|
|
214
|
+
return published;
|
|
215
|
+
};
|
|
216
|
+
const verbs = {};
|
|
217
|
+
for (let at = 0; at < BETTER_AUTH_VERB_NAMES.length; at += 1) {
|
|
218
|
+
const verb = BETTER_AUTH_VERB_NAMES[at];
|
|
219
|
+
const route = BETTER_AUTH_VERBS[verb];
|
|
220
|
+
if (!offered[verb] ||
|
|
221
|
+
!servesRoute(kind, route.path) ||
|
|
222
|
+
disabled.includes(route.path)) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
verbs[verb] = routed(verb, route, verb !== "signOut", (args) => verb === "signOut" ? undefined : identifierOf(verb, args));
|
|
226
|
+
}
|
|
227
|
+
if (!disabled.includes(ME_PATH)) {
|
|
228
|
+
const me = {
|
|
229
|
+
method: "GET",
|
|
230
|
+
path: ME_PATH,
|
|
231
|
+
result: meSchema(input.claims),
|
|
232
|
+
call: async ({ args, headers, request, }) => {
|
|
233
|
+
argsChecked("me", undefined, args);
|
|
234
|
+
return input.identityOf(headers, request);
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
freeze(me);
|
|
238
|
+
verbs.me = me;
|
|
239
|
+
}
|
|
240
|
+
if (kind === "kiosk") {
|
|
241
|
+
for (const verb of Object.keys(KIOSK_VERBS)) {
|
|
242
|
+
const route = KIOSK_VERBS[verb];
|
|
243
|
+
if (!disabled.includes(route.path)) {
|
|
244
|
+
verbs[verb] = routed(verb, route, verb !== "lock", () => undefined);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
freeze(verbs);
|
|
249
|
+
const client = { url, verbs };
|
|
250
|
+
freeze(client);
|
|
251
|
+
return client;
|
|
252
|
+
};
|
|
253
|
+
/**
|
|
254
|
+
* Access's `GET /me` route (under Better Auth's base path), keyed as Better Auth keys
|
|
255
|
+
* a plugin's endpoints (`access.api.accessMe`): the request's identity, `{ sub,
|
|
256
|
+
* claims }`, or `null`, as `access.client.verbs.me.call` answers it. Never cached.
|
|
257
|
+
*/
|
|
258
|
+
export const meEndpoint = (endpoint, identityOf) => ({
|
|
259
|
+
accessMe: endpoint(ME_PATH, { method: "GET" }, async (context) => {
|
|
260
|
+
const identity = await identityOf(context.headers ?? new Headers(), context.request);
|
|
261
|
+
return new Response(JSON.stringify(identity), {
|
|
262
|
+
status: 200,
|
|
263
|
+
headers: {
|
|
264
|
+
"content-type": "application/json",
|
|
265
|
+
"cache-control": "no-store",
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
}),
|
|
269
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { AccessClaimsDeclaration } from "../access/claims.mapper.js";
|
|
2
|
+
/** A verb `$access` offers through Access's routes (`can` is the framework's own). */
|
|
3
|
+
export type AccessVerbName = "signIn" | "signUp" | "signOut" | "resetPassword" | "me";
|
|
4
|
+
/** A closed object, in the ClientContract's form: its properties, the required ones, and no other key. */
|
|
5
|
+
type ObjectSchema = {
|
|
6
|
+
readonly type: "object";
|
|
7
|
+
readonly properties: {
|
|
8
|
+
readonly [key: string]: AccessVerbSchema;
|
|
9
|
+
};
|
|
10
|
+
readonly required?: readonly string[];
|
|
11
|
+
readonly additionalProperties: false;
|
|
12
|
+
};
|
|
13
|
+
/** A JSON Schema in the ClientContract's forms, as Access writes them: scalars and closed objects. */
|
|
14
|
+
export type AccessVerbSchema = {
|
|
15
|
+
readonly type: "string" | "number" | "boolean";
|
|
16
|
+
} | ObjectSchema;
|
|
17
|
+
/** One verb's route: the method, the path under Better Auth's base path, and the schemas. */
|
|
18
|
+
export type AccessVerbRoute = {
|
|
19
|
+
readonly method: "GET" | "POST";
|
|
20
|
+
readonly path: string;
|
|
21
|
+
readonly args?: ObjectSchema;
|
|
22
|
+
readonly result: ObjectSchema;
|
|
23
|
+
/** The key of Better Auth's server API (`access.api.<key>`) the verb's server call runs. */
|
|
24
|
+
readonly endpoint: "signInEmail" | "signUpEmail" | "signOut" | "resetPassword" | "kioskSwitchUser" | "kioskLock" | "kioskSetPin" | "kioskEnrollDevice" | "kioskRevokeDevice";
|
|
25
|
+
};
|
|
26
|
+
export declare const STRING: {
|
|
27
|
+
readonly type: "string";
|
|
28
|
+
};
|
|
29
|
+
export declare const BOOLEAN: {
|
|
30
|
+
readonly type: "boolean";
|
|
31
|
+
};
|
|
32
|
+
/** A closed object schema: its properties, the required ones, and no other key. */
|
|
33
|
+
export declare const closed: (properties: {
|
|
34
|
+
readonly [key: string]: AccessVerbSchema;
|
|
35
|
+
}, required: readonly string[]) => ObjectSchema;
|
|
36
|
+
/** The signed-in user, as a result names it: its id, the token's `sub`, and nothing of its record. */
|
|
37
|
+
export declare const USER: ObjectSchema;
|
|
38
|
+
/**
|
|
39
|
+
* The verbs on Better Auth's routes (better-auth 1.7.12 to 1.7.14): the arguments its client
|
|
40
|
+
* sends on each, and what Access promises of each answer.
|
|
41
|
+
*/
|
|
42
|
+
export declare const BETTER_AUTH_VERBS: {
|
|
43
|
+
readonly signIn: AccessVerbRoute;
|
|
44
|
+
readonly signUp: AccessVerbRoute;
|
|
45
|
+
readonly signOut: AccessVerbRoute;
|
|
46
|
+
readonly resetPassword: AccessVerbRoute;
|
|
47
|
+
};
|
|
48
|
+
/** Access's own route for `me`, under Better Auth's base path. */
|
|
49
|
+
export declare const ME_PATH = "/me";
|
|
50
|
+
/**
|
|
51
|
+
* `me`'s result: the token identity, `sub` and the declared claims, each typed as
|
|
52
|
+
* declared and each optional (a user may hold none of them). Nothing else.
|
|
53
|
+
*/
|
|
54
|
+
export declare const meSchema: (claims: AccessClaimsDeclaration) => ObjectSchema;
|
|
55
|
+
/**
|
|
56
|
+
* Why `value` does not fit `schema`, or `undefined` when it does: a key the schema
|
|
57
|
+
* does not name, a required key missing, or a value of another type. The sentence
|
|
58
|
+
* names the key and the type, never the value.
|
|
59
|
+
*/
|
|
60
|
+
export declare const schemaDefect: (schema: AccessVerbSchema, value: unknown, at: string) => string | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* Better Auth's answer, cut to what `schema` names: a key it does not name (the
|
|
63
|
+
* session token, a user field) is left out, and so is a value of another type.
|
|
64
|
+
*/
|
|
65
|
+
export declare const projected: (schema: AccessVerbSchema, value: unknown) => unknown;
|
|
66
|
+
export {};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// The `$access` verbs' routes and shapes: what `access.client` publishes for the
|
|
2
|
+
// framework's clients. Each verb is one of Better Auth's routes (its method and its
|
|
3
|
+
// path under Better Auth's base path), except `me`, which is Access's own. Its
|
|
4
|
+
// arguments are the ones Better Auth's client takes on that route
|
|
5
|
+
// (`authClient.signIn.email({ email, password })`), and its result names what Access
|
|
6
|
+
// promises of the answer: the signed-in user's id (`sub`) and Better Auth's own
|
|
7
|
+
// flags, never a session token or a user field. `me`'s result is the token identity,
|
|
8
|
+
// `{ sub, claims }`, with the claims `policies.claims` declares and nothing else.
|
|
9
|
+
//
|
|
10
|
+
// The schemas are written in the JSON Schema forms the framework's ClientContract
|
|
11
|
+
// takes (`validateClientContractStructure`): every object closed
|
|
12
|
+
// (`additionalProperties: false`), an optional key left out of `required`, and no
|
|
13
|
+
// keyword beyond those forms.
|
|
14
|
+
import { isRecord } from "../policies.compiler.js";
|
|
15
|
+
export const STRING = { type: "string" };
|
|
16
|
+
export const BOOLEAN = { type: "boolean" };
|
|
17
|
+
/** A closed object schema: its properties, the required ones, and no other key. */
|
|
18
|
+
export const closed = (properties, required) => required.length === 0
|
|
19
|
+
? { type: "object", properties, additionalProperties: false }
|
|
20
|
+
: { type: "object", properties, required, additionalProperties: false };
|
|
21
|
+
/** The signed-in user, as a result names it: its id, the token's `sub`, and nothing of its record. */
|
|
22
|
+
export const USER = closed({ id: STRING }, ["id"]);
|
|
23
|
+
/**
|
|
24
|
+
* The verbs on Better Auth's routes (better-auth 1.7.12 to 1.7.14): the arguments its client
|
|
25
|
+
* sends on each, and what Access promises of each answer.
|
|
26
|
+
*/
|
|
27
|
+
export const BETTER_AUTH_VERBS = {
|
|
28
|
+
signIn: {
|
|
29
|
+
method: "POST",
|
|
30
|
+
path: "/sign-in/email",
|
|
31
|
+
args: closed({
|
|
32
|
+
email: STRING,
|
|
33
|
+
password: STRING,
|
|
34
|
+
callbackURL: STRING,
|
|
35
|
+
rememberMe: BOOLEAN,
|
|
36
|
+
}, ["email", "password"]),
|
|
37
|
+
result: closed({ redirect: BOOLEAN, url: STRING, user: USER }, [
|
|
38
|
+
"redirect",
|
|
39
|
+
"user",
|
|
40
|
+
]),
|
|
41
|
+
endpoint: "signInEmail",
|
|
42
|
+
},
|
|
43
|
+
signUp: {
|
|
44
|
+
method: "POST",
|
|
45
|
+
path: "/sign-up/email",
|
|
46
|
+
args: closed({
|
|
47
|
+
name: STRING,
|
|
48
|
+
email: STRING,
|
|
49
|
+
password: STRING,
|
|
50
|
+
image: STRING,
|
|
51
|
+
callbackURL: STRING,
|
|
52
|
+
rememberMe: BOOLEAN,
|
|
53
|
+
}, ["name", "email", "password"]),
|
|
54
|
+
result: closed({ user: USER }, ["user"]),
|
|
55
|
+
endpoint: "signUpEmail",
|
|
56
|
+
},
|
|
57
|
+
signOut: {
|
|
58
|
+
method: "POST",
|
|
59
|
+
path: "/sign-out",
|
|
60
|
+
result: closed({ success: BOOLEAN }, ["success"]),
|
|
61
|
+
endpoint: "signOut",
|
|
62
|
+
},
|
|
63
|
+
resetPassword: {
|
|
64
|
+
method: "POST",
|
|
65
|
+
path: "/reset-password",
|
|
66
|
+
args: closed({ newPassword: STRING, token: STRING }, [
|
|
67
|
+
"newPassword",
|
|
68
|
+
"token",
|
|
69
|
+
]),
|
|
70
|
+
result: closed({ status: BOOLEAN }, ["status"]),
|
|
71
|
+
endpoint: "resetPassword",
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
/** Access's own route for `me`, under Better Auth's base path. */
|
|
75
|
+
export const ME_PATH = "/me";
|
|
76
|
+
/**
|
|
77
|
+
* `me`'s result: the token identity, `sub` and the declared claims, each typed as
|
|
78
|
+
* declared and each optional (a user may hold none of them). Nothing else.
|
|
79
|
+
*/
|
|
80
|
+
export const meSchema = (claims) => {
|
|
81
|
+
const declared = {};
|
|
82
|
+
for (const claim of Object.keys(claims)) {
|
|
83
|
+
declared[claim] = {
|
|
84
|
+
type: claims[claim],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return closed({ sub: STRING, claims: closed(declared, []) }, [
|
|
88
|
+
"sub",
|
|
89
|
+
"claims",
|
|
90
|
+
]);
|
|
91
|
+
};
|
|
92
|
+
/** What a refused value is, never the value: arguments carry passwords. */
|
|
93
|
+
const kindOf = (value) => Array.isArray(value)
|
|
94
|
+
? "an array"
|
|
95
|
+
: value === null
|
|
96
|
+
? "null"
|
|
97
|
+
: typeof value === "object"
|
|
98
|
+
? "an object"
|
|
99
|
+
: `a ${typeof value}`;
|
|
100
|
+
/**
|
|
101
|
+
* Why `value` does not fit `schema`, or `undefined` when it does: a key the schema
|
|
102
|
+
* does not name, a required key missing, or a value of another type. The sentence
|
|
103
|
+
* names the key and the type, never the value.
|
|
104
|
+
*/
|
|
105
|
+
export const schemaDefect = (schema, value, at) => {
|
|
106
|
+
if (schema.type !== "object") {
|
|
107
|
+
return typeof value === schema.type
|
|
108
|
+
? undefined
|
|
109
|
+
: `${at} is ${kindOf(value)}, not a ${schema.type}`;
|
|
110
|
+
}
|
|
111
|
+
if (!isRecord(value)) {
|
|
112
|
+
return `${at} is ${kindOf(value)}, not an object`;
|
|
113
|
+
}
|
|
114
|
+
for (const key of Object.keys(value)) {
|
|
115
|
+
if (!Object.hasOwn(schema.properties, key)) {
|
|
116
|
+
return `${at} has \`${key}\`, which it does not take (it takes ${Object.keys(schema.properties).join(", ") || "nothing"})`;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const required = schema.required;
|
|
120
|
+
for (let index = 0; required !== undefined && index < required.length; index += 1) {
|
|
121
|
+
const key = required[index];
|
|
122
|
+
if (value[key] === undefined) {
|
|
123
|
+
return `${at} lacks \`${key}\``;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
for (const key of Object.keys(schema.properties)) {
|
|
127
|
+
const member = value[key];
|
|
128
|
+
const property = schema.properties[key];
|
|
129
|
+
if (member === undefined || property === undefined) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const defect = schemaDefect(property, member, `${at}.${key}`);
|
|
133
|
+
if (defect !== undefined) {
|
|
134
|
+
return defect;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return undefined;
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* Better Auth's answer, cut to what `schema` names: a key it does not name (the
|
|
141
|
+
* session token, a user field) is left out, and so is a value of another type.
|
|
142
|
+
*/
|
|
143
|
+
export const projected = (schema, value) => {
|
|
144
|
+
if (schema.type !== "object") {
|
|
145
|
+
return typeof value === schema.type ? value : undefined;
|
|
146
|
+
}
|
|
147
|
+
if (!isRecord(value)) {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
const kept = {};
|
|
151
|
+
for (const key of Object.keys(schema.properties)) {
|
|
152
|
+
const property = schema.properties[key];
|
|
153
|
+
const member = property === undefined ? undefined : projected(property, value[key]);
|
|
154
|
+
if (member !== undefined) {
|
|
155
|
+
kept[key] = member;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return kept;
|
|
159
|
+
};
|