@aotter/mantle 0.2.0-alpha.7 → 0.2.0-alpha.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -5
- package/dist/auth/caller.js +2 -1
- package/dist/auth/identity.d.ts +2 -2
- package/dist/auth/oauth.d.ts +33 -1
- package/dist/auth/oauth.js +121 -28
- package/dist/auth/routes.js +3 -2
- package/dist/build/compiler.d.ts +44 -122
- package/dist/build/compiler.js +202 -277
- package/dist/build/spec/domain/model/ManifestGrammar.d.ts +6 -5
- package/dist/build/spec/domain/model/SqlIr.d.ts +2 -0
- package/dist/build/spec/domain/service/PlanFingerprint.d.ts +2 -0
- package/dist/build/spec/kernel/diagnostic.d.ts +1 -1
- package/dist/domain/describe/{chunk-ZIQVLA7X.js → chunk-ZVXBGP7Y.js} +69 -8
- package/dist/domain/describe/index.js +1 -1
- package/dist/domain/describe/target-hash.js +1 -1
- package/dist/domain/describe/targets.d.ts +1 -1
- package/dist/domain/generated-tools.js +2 -0
- package/dist/domain/paging.js +41 -15
- package/dist/infrastructure/bundle.js +105 -15
- package/dist/infrastructure/cli.js +11 -15
- package/dist/infrastructure/generate.d.ts +1 -3
- package/dist/infrastructure/generate.js +2 -2
- package/dist/infrastructure/generateAuth.js +6 -0
- package/dist/infrastructure/inputs.js +8 -3
- package/dist/infrastructure/wrangler.js +35 -12
- package/dist/kernel/drain.d.ts +5 -0
- package/dist/kernel/drain.js +35 -0
- package/dist/kernel/driver/postgres.js +3 -0
- package/dist/kernel/host-caller.d.ts +1 -0
- package/dist/kernel/host-caller.js +1 -0
- package/dist/kernel/mcp-apps.d.ts +29 -9
- package/dist/kernel/mcp-apps.js +2 -2
- package/dist/kernel/mcp-handler.js +100 -17
- package/dist/kernel/mcp.d.ts +7 -2
- package/dist/kernel/mcp.js +29 -5
- package/dist/kernel/rest-router.js +3 -2
- package/dist/kernel/site.d.ts +1 -3
- package/dist/kernel/site.js +2 -2
- package/dist/kernel/web.d.ts +3 -1
- package/dist/kernel/web.js +3 -0
- package/dist/usecase/adminBindings.js +2 -3
- package/dist/usecase/adminClient.js +87 -0
- package/dist/usecase/adminDescriptors.js +19 -65
- package/dist/usecase/generate.js +5 -4
- package/dist/usecase/mcp-media.js +2 -2
- package/dist/usecase/mcp.js +16 -12
- package/dist/usecase/paging.js +2 -3
- package/dist/usecase/publishing.js +2 -2
- package/dist/usecase/tool-version.js +9 -0
- package/dist/usecase/translations.js +1 -1
- package/dist/usecase/validation.js +120 -19
- package/dist/usecase/web.js +3 -2
- package/docs/examples/README.md +1 -1
- package/docs/examples/app-shell/README.md +39 -1
- package/docs/examples/app-shell/package.json +14 -6
- package/docs/examples/app-shell/smoke-kit.mjs +54 -0
- package/docs/examples/app-shell/src/auth.ts +4 -2
- package/docs/examples/app-shell/src/service.ts +2 -2
- package/docs/examples/app-shell/tsconfig.json +4 -1
- package/docs/examples/app-shell/worker-configuration.d.ts +2 -2
- package/docs/examples/app-shell/wrangler.jsonc +5 -4
- package/docs/examples/generated-web/README.md +6 -2
- package/docs/examples/generated-web/wrangler.jsonc +1 -0
- package/docs/examples/purchasing/README.md +1 -1
- package/docs/examples/purchasing/manifests/purchasing.yaml +1 -1
- package/docs/examples/purchasing/smoke.mjs +10 -4
- package/docs/examples/purchasing/src/admin.tsx +13 -0
- package/docs/examples/purchasing/wrangler.jsonc +5 -4
- package/docs/examples/recipes/wrangler.jsonc +5 -4
- package/docs/handbook/cloudflare/authentication.md +22 -2
- package/docs/handbook/cloudflare/deploy-and-operate.md +21 -0
- package/docs/handbook/cloudflare/service-entry.md +55 -28
- package/docs/handbook/concepts/authorization.md +1 -1
- package/docs/handbook/concepts/mcp-and-agents.md +19 -0
- package/docs/handbook/examples/hub.md +1 -1
- package/docs/handbook/guides/admin-ui.md +107 -1
- package/docs/handbook/guides/agent-setup.md +27 -9
- package/docs/handbook/guides/mcp-apps.md +445 -6
- package/docs/handbook/guides/native-admin.md +296 -51
- package/docs/handbook/guides/oauth.md +61 -5
- package/docs/handbook/guides/public-pages.md +2 -2
- package/docs/handbook/guides/size-and-startup.md +1 -1
- package/docs/handbook/navigation.json +0 -4
- package/docs/handbook/reference/admin-descriptors.md +0 -35
- package/docs/handbook/reference/procedure.md +5 -1
- package/docs/handbook/reference/schema.md +13 -3
- package/docs/handbook/reference/surface.md +123 -13
- package/docs/handbook/reference/trigger.md +34 -1
- package/docs/handbook/reference/view.md +1 -1
- package/docs/handbook/releases/index.md +74 -2
- package/docs/handbook/start/overview.md +1 -1
- package/docs/handbook/start/project-and-cli.md +8 -8
- package/docs/handbook/start/quickstart-worker.md +1 -1
- package/docs/skills/develop/SKILL.md +4 -3
- package/docs/skills/integrate/SKILL.md +1 -1
- package/docs/skills/media-gc/SKILL.md +7 -5
- package/docs/skills/provision/SKILL.md +4 -1
- package/docs/skills/update/SKILL.md +1 -1
- package/docs/upgrade-0.1-to-0.2.md +6 -6
- package/package.json +2 -1
- package/skills/README.md +11 -16
- package/skills/mantle/SKILL.md +4 -4
- package/skills/mantle/references/local-testing.md +4 -0
- package/skills/mantle/references/web.md +1 -1
- package/skills/mantle/references/website.md +24 -22
- package/docs/examples/app-shell/src/admin.ts +0 -10
- package/docs/examples/purchasing/src/admin.ts +0 -12
- package/docs/handbook/guides/admin-extensions.md +0 -20
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ For transports, `paged/views/<name>.ts` returns `{ rows, nextCursor? }` with key
|
|
|
34
34
|
[surface deltas](./docs/handbook/reference/surface.md) define the REST/MCP wire contract.
|
|
35
35
|
Proved internal Views also get paging without a public route/tool: one SELECT
|
|
36
36
|
per page, validated scalar cursor keys and hidden-key removal. Schema tuples
|
|
37
|
-
need indexed order after fixed equality prefixes; complete groups and singleton
|
|
37
|
+
need indexed order after fixed equality prefixes ([index prefixes](./docs/handbook/reference/schema.md#index-prefixes-for-paged-views)); complete groups and singleton
|
|
38
38
|
joins are supported. Grouping still scans/aggregates its source: inspect EXPLAIN.
|
|
39
39
|
Unsupported internal SQL retains its bare module.
|
|
40
40
|
|
|
@@ -97,16 +97,21 @@ For `cloudflare-d1` and `cloudflare-postgres`, generation configures Wrangler to
|
|
|
97
97
|
`mantle bundle <entry> <outdir>` automatically. Node 22.22 or newer is required. Run from the project root;
|
|
98
98
|
regenerate after manifest or auth option changes.
|
|
99
99
|
|
|
100
|
-
`--entry` defaults to `src/worker.ts`, `--outdir` to `dist`. `--wrangler` defaults to the
|
|
100
|
+
`--entry` defaults to `src/worker.ts`, `--outdir` to `dist`; with `--admin`, `--assets` (Wrangler's `assets.directory`) defaults to `<outdir>/client`. `--wrangler` defaults to the
|
|
101
101
|
existing `wrangler.jsonc`, `wrangler.json` or `wrangler.toml`, otherwise `wrangler.jsonc`.
|
|
102
102
|
JSONC edits preserve comments and unrelated settings and are idempotent. A new config
|
|
103
103
|
needs its `name`, `compatibility_date` and bindings filled in. TOML only prints instructions.
|
|
104
104
|
|
|
105
|
-
esbuild emits minified split ESM
|
|
106
|
-
`find_additional_modules`, `base_dir` and an ESModule rule.
|
|
105
|
+
esbuild emits minified split ESM into `<outdir>/worker`; Wrangler uploads it with `no_bundle`,
|
|
106
|
+
`find_additional_modules`, `base_dir` and an ESModule rule. Generation ensures
|
|
107
|
+
`nodejs_compat` and defaults to `global_fetch_strictly_public`, including explicit
|
|
108
|
+
environment flag overrides; see [Workers deployment](./docs/handbook/cloudflare/deploy-and-operate.md).
|
|
107
109
|
The metafile guard rejects `better-auth` / `@better-auth/*` in the static startup graph:
|
|
108
110
|
use `authLoader(() => import(...))` ([Auth](./docs/handbook/cloudflare/authentication.md#worker-entry)).
|
|
109
|
-
The
|
|
111
|
+
The Worker directory is cleaned first; keep source and generated code elsewhere. A Worker that imports
|
|
112
|
+
`@aotter/mantle-admin/server` also gets Admin, built with Vite into `<assets>/admin` (default `<outdir>/client/admin`)
|
|
113
|
+
from `admin.tsx` beside the entry or the built-in entry. The built-in entry is skipped while the installed
|
|
114
|
+
Admin and its direct dependencies are unchanged; an `admin.tsx` entry builds every time.
|
|
110
115
|
|
|
111
116
|
## D1 Sessions
|
|
112
117
|
|
package/dist/auth/caller.js
CHANGED
|
@@ -91,7 +91,8 @@ export async function resolveCaller(request, auth, options = {}) {
|
|
|
91
91
|
const { oauthProviderResourceClient } = await import("@better-auth/oauth-provider/resource-client");
|
|
92
92
|
const { createDpopReplayStore } = await import("better-auth/oauth2");
|
|
93
93
|
const context = await instance.$context;
|
|
94
|
-
|
|
94
|
+
// Issuance uses the normalized auth context URL, not the raw origin-only baseURL option.
|
|
95
|
+
const verifier = oauthProviderResourceClient({ options: { ...instance.options, baseURL: context.baseURL, basePath: "" }, $context: instance.$context }).getActions();
|
|
95
96
|
let claims;
|
|
96
97
|
try {
|
|
97
98
|
claims = await verifier.verifyAccessTokenRequest(request, { verifyOptions: { audience: options.audience }, dpop: { replayStore: createDpopReplayStore(context.internalAdapter) } });
|
package/dist/auth/identity.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Optional identity operations. Better Auth owns access control, writes and user hooks. */
|
|
2
2
|
import { type AuthInstance } from "./caller.js";
|
|
3
|
-
import { type Caller, type Postgres } from "../kernel/contracts.js";
|
|
3
|
+
import { type Caller, type Postgres, type Unsafe } from "../kernel/contracts.js";
|
|
4
4
|
import type { Dialect } from "../kernel/dialect/common.js";
|
|
5
5
|
import { type Reader } from "../kernel/driver/d1.js";
|
|
6
6
|
type User = {
|
|
@@ -94,7 +94,7 @@ declare function directory(auth: IdentityAuth, _caller: Caller, headers: Headers
|
|
|
94
94
|
};
|
|
95
95
|
export type IdentityDirectory = ReturnType<typeof directory>;
|
|
96
96
|
export declare const d1Identity: (db: Reader, auth: IdentityAuth, caller: Caller, headers: Headers) => IdentityDirectory;
|
|
97
|
-
export declare const postgresIdentity: (db:
|
|
97
|
+
export declare const postgresIdentity: (db: Unsafe, auth: IdentityAuth, caller: Caller, headers: Headers) => IdentityDirectory;
|
|
98
98
|
/**
|
|
99
99
|
* Server-side reads of Better Auth users for code without a session (cron, system callers): no SQL against Better Auth's
|
|
100
100
|
* tables, each instance's own table mapping, banned users excluded. Read-only; writes stay with the caller-bound directory.
|
package/dist/auth/oauth.d.ts
CHANGED
|
@@ -1,11 +1,43 @@
|
|
|
1
1
|
import type { oauthProvider } from "@better-auth/oauth-provider";
|
|
2
2
|
import type { AuthInstance } from "./caller.js";
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Fail closed for unbound tokens; reconsent cannot resurrect an issued JWT. As in Better Auth, an `offline_access` grant outlives
|
|
5
|
+
* the browser session: without a live session it stands on an unrevoked, unexpired offline refresh token for the same user and client.
|
|
6
|
+
*/
|
|
4
7
|
export declare function assertActiveUserGrant(auth: AuthInstance, claims: Record<string, unknown>, audience: string | readonly string[]): Promise<{
|
|
5
8
|
id: string;
|
|
6
9
|
}>;
|
|
7
10
|
/** Add first to oauthProvider/mcp extensions; Better Auth retains ownership of referenceId. */
|
|
8
11
|
export declare const oauthGrantExtension: NonNullable<Parameters<typeof oauthProvider>[0]["extensions"]>[number];
|
|
12
|
+
export declare const OAUTH_GRANT_REVOCATION = "mantle-oauth-grant-revocation";
|
|
13
|
+
/**
|
|
14
|
+
* Add to the Better Auth plugins with the OAuth provider. An `offline_access` grant outlives sign-out and session expiry
|
|
15
|
+
* (as in Better Auth), but a successful password reset, a password change that signs out other devices, an admin ban
|
|
16
|
+
* (`/admin/ban-user`, or `/admin/update-user` setting `banned: true`), an admin session revocation or an admin password
|
|
17
|
+
* change ends every OAuth grant of that user: tokens, consents and unredeemed codes. So does an email OTP or magic-link
|
|
18
|
+
* sign-in attempt on an unverified user: Better Auth strips what was set up before that proof, possibly by someone else
|
|
19
|
+
* who registered the address first, and Mantle ends that party's grants before the attempt is checked.
|
|
20
|
+
*/
|
|
21
|
+
export declare function oauthGrantRevocation(): {
|
|
22
|
+
id: "mantle-oauth-grant-revocation";
|
|
23
|
+
hooks: {
|
|
24
|
+
before: ({
|
|
25
|
+
matcher: (ctx: import("better-auth").HookEndpointContext) => boolean;
|
|
26
|
+
handler: import("better-auth").Middleware<import("better-auth").MiddlewareOptions, (inputContext: import("better-auth").MiddlewareInputContext<import("better-auth").MiddlewareOptions>) => Promise<{
|
|
27
|
+
context: {
|
|
28
|
+
mantleResetPasswordUser: string;
|
|
29
|
+
};
|
|
30
|
+
} | undefined>>;
|
|
31
|
+
} | {
|
|
32
|
+
matcher: (ctx: import("better-auth").HookEndpointContext) => boolean;
|
|
33
|
+
handler: import("better-auth").Middleware<import("better-auth").MiddlewareOptions, (inputContext: import("better-auth").MiddlewareInputContext<import("better-auth").MiddlewareOptions>) => Promise<void>>;
|
|
34
|
+
})[];
|
|
35
|
+
after: {
|
|
36
|
+
matcher: (ctx: import("better-auth").HookEndpointContext) => boolean;
|
|
37
|
+
handler: import("better-auth").Middleware<import("better-auth").MiddlewareOptions, (inputContext: import("better-auth").MiddlewareInputContext<import("better-auth").MiddlewareOptions>) => Promise<void>>;
|
|
38
|
+
}[];
|
|
39
|
+
};
|
|
40
|
+
};
|
|
9
41
|
export interface OAuthConsentRequest {
|
|
10
42
|
readonly clientName: string;
|
|
11
43
|
readonly redirectUri: string;
|
package/dist/auth/oauth.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createAuthMiddleware } from "@better-auth/core/api";
|
|
1
2
|
import { nativeAuthRead } from "./adapter.js";
|
|
2
3
|
import { ContractError } from "../kernel/contracts.js";
|
|
3
4
|
const pair = (field, value) => ({ field, value });
|
|
@@ -10,39 +11,147 @@ const array = (value) => {
|
|
|
10
11
|
return null;
|
|
11
12
|
}
|
|
12
13
|
};
|
|
13
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Fail closed for unbound tokens; reconsent cannot resurrect an issued JWT. As in Better Auth, an `offline_access` grant outlives
|
|
16
|
+
* the browser session: without a live session it stands on an unrevoked, unexpired offline refresh token for the same user and client.
|
|
17
|
+
*/
|
|
14
18
|
export async function assertActiveUserGrant(auth, claims, audience) {
|
|
15
19
|
const { sub, azp, sid, mantle_consent_id: id, mantle_consent_reference: reference, mantle_skip_consent: skip } = claims;
|
|
16
|
-
|
|
20
|
+
const tokenScopes = typeof claims.scope === "string" ? claims.scope.split(/\s+/).filter(Boolean) : array(claims.scope);
|
|
21
|
+
const offline = !!tokenScopes?.includes("offline_access");
|
|
22
|
+
if (![sub, azp].every(x => typeof x === "string" && x.length) || !(offline && sid == null || typeof sid === "string" && sid.length) || !(skip === true && id === null || typeof id === "string" && id.length))
|
|
17
23
|
throw new ContractError("UNAUTHENTICATED", "OAuth token is not bound to a user session.");
|
|
18
24
|
const { exec, param, now, table, column } = nativeAuthRead(auth.options);
|
|
19
|
-
const
|
|
20
|
-
const
|
|
25
|
+
const at = now(Date.now());
|
|
26
|
+
const c = (field) => `c.${column("oauthConsent", field)}`, s = (field) => `s.${column("session", field)}`, client = (field) => `client.${column("oauthClient", field)}`, r = (field) => column("oauthRefreshToken", field);
|
|
27
|
+
const { rows } = await exec(`SELECT ${s("id")} AS session, ${c("id")} AS "consentId", ${c("referenceId")} AS reference, ${c("resources")} AS resources, ${c("scopes")} AS scopes,
|
|
21
28
|
${client("skipConsent")} AS "skipConsent", ${client("disabled")} AS disabled, ${client("scopes")} AS "clientScopes"
|
|
22
|
-
FROM ${table("oauthClient")} client JOIN ${table("session")} s ON ${s("id")} = ${param(1)} AND ${s("userId")} = ${param(2)} AND ${s("expiresAt")} > ${param(3)}
|
|
23
|
-
LEFT JOIN ${table("oauthConsent")} c ON ${c("id")} = ${param(4)} AND ${c("userId")} = ${
|
|
24
|
-
WHERE ${client("clientId")} = ${param(5)}`, [sid, sub,
|
|
29
|
+
FROM ${table("oauthClient")} client LEFT JOIN ${table("session")} s ON ${s("id")} = ${param(1)} AND ${s("userId")} = ${param(2)} AND ${s("expiresAt")} > ${param(3)}
|
|
30
|
+
LEFT JOIN ${table("oauthConsent")} c ON ${c("id")} = ${param(4)} AND ${c("userId")} = ${param(2)} AND ${c("clientId")} = ${client("clientId")}
|
|
31
|
+
WHERE ${client("clientId")} = ${param(5)}`, [sid ?? null, sub, at, id, azp]);
|
|
25
32
|
const grant = rows[0];
|
|
33
|
+
const live = !!grant?.session || offline && !!grant && (await exec(`SELECT ${r("scopes")} AS scopes FROM ${table("oauthRefreshToken")}
|
|
34
|
+
WHERE ${r("userId")} = ${param(1)} AND ${r("clientId")} = ${param(2)} AND ${r("revoked")} IS NULL AND ${r("expiresAt")} > ${param(3)}`, [sub, azp, at])).rows.some(row => array(row.scopes)?.includes("offline_access"));
|
|
26
35
|
const provider = auth.options.plugins?.find(plugin => plugin.id === "oauth-provider");
|
|
27
36
|
const firstParty = skip === true && id === null && !!grant?.skipConsent;
|
|
28
37
|
const resources = firstParty ? (typeof claims.aud === "string" ? [claims.aud] : array(claims.aud)) : array(grant?.resources);
|
|
29
38
|
const scopes = firstParty ? array(grant?.clientScopes) ?? provider?.options?.scopes : array(grant?.scopes);
|
|
30
39
|
const requested = typeof audience === "string" ? [audience] : audience;
|
|
31
40
|
const tokenAudiences = typeof claims.aud === "string" ? [claims.aud] : array(claims.aud) ?? [];
|
|
32
|
-
|
|
33
|
-
if (!grant || grant.disabled || !firstParty && (!grant.consentId || (grant.reference ?? null) !== reference) || !resources || !scopes || !tokenScopes || !requested.some(x => resources.includes(x) && tokenAudiences.includes(x)) || !tokenScopes.every(x => scopes.includes(x)))
|
|
41
|
+
if (!grant || !live || grant.disabled || !firstParty && (!grant.consentId || (grant.reference ?? null) !== reference) || !resources || !scopes || !tokenScopes || !requested.some(x => resources.includes(x) && tokenAudiences.includes(x)) || !tokenScopes.every(x => scopes.includes(x)))
|
|
34
42
|
throw new ContractError("UNAUTHENTICATED", "OAuth authorization grant is no longer active.");
|
|
35
|
-
return { id:
|
|
43
|
+
return { id: sub };
|
|
36
44
|
}
|
|
37
45
|
/** Add first to oauthProvider/mcp extensions; Better Auth retains ownership of referenceId. */
|
|
38
46
|
export const oauthGrantExtension = {
|
|
39
|
-
claims: { accessToken: async ({ ctx, user, client, referenceId }) => {
|
|
47
|
+
claims: { accessToken: async ({ ctx, user, client, referenceId, grantType, scopes, sessionId }) => {
|
|
48
|
+
// Narrowing away offline_access on refresh would mint a session-bound token for a session that has already ended.
|
|
49
|
+
if (grantType === "refresh_token" && !scopes.includes("offline_access")) {
|
|
50
|
+
const session = sessionId ? await ctx.context.adapter.findOne({ model: "session", where: [pair("id", sessionId)] }) : null;
|
|
51
|
+
if (!session || new Date(session.expiresAt).getTime() <= Date.now())
|
|
52
|
+
throw ctx.error("BAD_REQUEST", { error: "invalid_grant", error_description: "The browser session for this grant has ended; keep offline_access or sign in again." });
|
|
53
|
+
}
|
|
40
54
|
if (client.skipConsent)
|
|
41
55
|
return { mantle_consent_id: null, mantle_skip_consent: true };
|
|
42
56
|
const consent = user && await ctx.context.adapter.findOne({ model: "oauthConsent", where: [pair("userId", user.id), pair("clientId", client.clientId), { field: "referenceId", value: referenceId ?? null }] });
|
|
43
57
|
return { mantle_consent_id: consent?.id ?? null, mantle_consent_reference: referenceId ?? null };
|
|
44
58
|
} },
|
|
45
59
|
};
|
|
60
|
+
export const OAUTH_GRANT_REVOCATION = "mantle-oauth-grant-revocation";
|
|
61
|
+
/** Password resets (all channels) and an admin password change: the old credential may be compromised, so every session ends too. */
|
|
62
|
+
const RESETS = ["/reset-password", "/email-otp/reset-password", "/phone-number/reset-password", "/admin/set-user-password"];
|
|
63
|
+
const ENDING = [...RESETS, "/change-password", "/admin/ban-user", "/admin/update-user", "/admin/revoke-user-sessions"];
|
|
64
|
+
const RESET_USER = "mantleResetPasswordUser";
|
|
65
|
+
/** Better Auth's email-proof sign-ins: on an unverified user they delete its accounts and sessions (revokeUnprovenAccountAccess). */
|
|
66
|
+
const PROVING = ["/sign-in/email-otp", "/magic-link/verify"];
|
|
67
|
+
/**
|
|
68
|
+
* Add to the Better Auth plugins with the OAuth provider. An `offline_access` grant outlives sign-out and session expiry
|
|
69
|
+
* (as in Better Auth), but a successful password reset, a password change that signs out other devices, an admin ban
|
|
70
|
+
* (`/admin/ban-user`, or `/admin/update-user` setting `banned: true`), an admin session revocation or an admin password
|
|
71
|
+
* change ends every OAuth grant of that user: tokens, consents and unredeemed codes. So does an email OTP or magic-link
|
|
72
|
+
* sign-in attempt on an unverified user: Better Auth strips what was set up before that proof, possibly by someone else
|
|
73
|
+
* who registered the address first, and Mantle ends that party's grants before the attempt is checked.
|
|
74
|
+
*/
|
|
75
|
+
export function oauthGrantRevocation() {
|
|
76
|
+
return {
|
|
77
|
+
id: OAUTH_GRANT_REVOCATION,
|
|
78
|
+
hooks: {
|
|
79
|
+
// Better Auth consumes the reset token, so its user rides this request's own context to the after hook.
|
|
80
|
+
before: [{ matcher: ctx => ctx.path === "/reset-password", handler: createAuthMiddleware(async (ctx) => {
|
|
81
|
+
// Better Auth reads the token from the body or the query, in that order.
|
|
82
|
+
const token = ctx.body?.token || ctx.query?.token;
|
|
83
|
+
const value = typeof token === "string" && token ? await ctx.context.internalAdapter.findVerificationValue(`reset-password:${token}`) : null;
|
|
84
|
+
if (value)
|
|
85
|
+
return { context: { [RESET_USER]: value.value } };
|
|
86
|
+
}) }, { matcher: ctx => PROVING.includes(ctx.path ?? ""), handler: createAuthMiddleware(async (ctx) => {
|
|
87
|
+
// Before the endpoint, while the user is still unverified and the magic-link token is unconsumed: once Better Auth
|
|
88
|
+
// marks the address verified, no later request would tell the pre-registrant's grants apart. A failure here fails
|
|
89
|
+
// the request before verification, so a retry ends them. A failed attempt ends them too: an unverified user's grants
|
|
90
|
+
// are exactly the pre-hijack risk.
|
|
91
|
+
const email = ctx.path === "/sign-in/email-otp" ? ctx.body?.email : await magicLinkEmail(ctx.context, ctx.query?.token);
|
|
92
|
+
const found = typeof email === "string" && email ? await ctx.context.internalAdapter.findUserByEmail(email.toLowerCase()) : null;
|
|
93
|
+
if (found && !found.user.emailVerified)
|
|
94
|
+
await endGrants(ctx.context.adapter, found.user.id);
|
|
95
|
+
}) }],
|
|
96
|
+
// After the endpoint: its own validation and permission checks have passed and it succeeded.
|
|
97
|
+
after: [{ matcher: ctx => ENDING.includes(ctx.path ?? ""), handler: createAuthMiddleware(async (ctx) => {
|
|
98
|
+
const returned = ctx.context.returned, path = ctx.path ?? "";
|
|
99
|
+
if (returned instanceof Error || returned instanceof Response && !returned.ok)
|
|
100
|
+
return;
|
|
101
|
+
const body = ctx.body;
|
|
102
|
+
const userId = path === "/reset-password" ? ctx[RESET_USER]
|
|
103
|
+
: path === "/email-otp/reset-password" ? typeof body?.email === "string" && (await ctx.context.internalAdapter.findUserByEmail(body.email.toLowerCase()))?.user.id
|
|
104
|
+
: path === "/phone-number/reset-password" ? typeof body?.phoneNumber === "string" && (await ctx.context.adapter.findOne({ model: "user", where: [pair("phoneNumber", body.phoneNumber)] }))?.id
|
|
105
|
+
// Better Auth itself ends the other sessions and keeps the current one.
|
|
106
|
+
: path === "/change-password" ? body?.revokeOtherSessions === true && (ctx.context.session?.user.id ?? returned?.user?.id)
|
|
107
|
+
: path === "/admin/update-user" ? body?.data?.banned === true && body.userId
|
|
108
|
+
: body?.userId;
|
|
109
|
+
if (typeof userId !== "string" || !userId)
|
|
110
|
+
return;
|
|
111
|
+
// Sessions first: a live session (perhaps the attacker's) could otherwise authorize a new grant after endGrants.
|
|
112
|
+
if (RESETS.includes(path))
|
|
113
|
+
await ctx.context.internalAdapter.deleteUserSessions(userId);
|
|
114
|
+
await endGrants(ctx.context.adapter, userId);
|
|
115
|
+
}) }],
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/** The email a magic-link token names, read without consuming it; the token is stored as the magic-link plugin's `storeToken` says. */
|
|
120
|
+
async function magicLinkEmail(context, token) {
|
|
121
|
+
if (typeof token !== "string" || !token)
|
|
122
|
+
return;
|
|
123
|
+
const store = context.options.plugins?.find(plugin => plugin.id === "magic-link")?.options?.storeToken ?? "plain";
|
|
124
|
+
const hash = typeof store === "object" && store && "hash" in store && typeof store.hash === "function" ? store.hash : undefined;
|
|
125
|
+
const stored = store === "plain" ? token : hash ? await hash(token)
|
|
126
|
+
: store === "hashed" ? btoa(String.fromCharCode(...new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(token))))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "") : undefined;
|
|
127
|
+
const value = stored === undefined ? null : await context.internalAdapter.findVerificationValue(`magic-link:${stored}`);
|
|
128
|
+
try {
|
|
129
|
+
return value ? JSON.parse(value.value).email : undefined;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/** Ends a user's grants (for one client, or all): consents, tokens and unredeemed authorization codes. */
|
|
136
|
+
async function endGrants(db, userId, clientId) {
|
|
137
|
+
const where = [pair("userId", userId), ...clientId ? [pair("clientId", clientId)] : []];
|
|
138
|
+
// Delete first: even if cleanup fails, every issued JWT fails the live-grant check immediately.
|
|
139
|
+
await db.deleteMany({ model: "oauthConsent", where });
|
|
140
|
+
for (const model of ["oauthRefreshToken", "oauthAccessToken"])
|
|
141
|
+
await db.updateMany({ model, where: [...where, { field: "revoked", value: null }], update: { revoked: new Date() } });
|
|
142
|
+
// An unredeemed code would mint a new token lineage after reconsent.
|
|
143
|
+
const code = (v) => { const x = authCode(v); return x?.userId === userId && (!clientId || x.query.client_id === clientId); };
|
|
144
|
+
const codes = [userId, clientId].filter(x => x !== undefined).map(x => ({ field: "value", operator: "contains", value: JSON.stringify(x) }));
|
|
145
|
+
for (let offset = 0;;) {
|
|
146
|
+
const page = await db.findMany({ model: "verification", where: codes, limit: 100, offset, sortBy: { field: "id", direction: "asc" } });
|
|
147
|
+
const ids = page.filter(v => code(v.value)).map(v => v.id);
|
|
148
|
+
if (ids.length)
|
|
149
|
+
await db.deleteMany({ model: "verification", where: [{ field: "id", operator: "in", value: ids }] });
|
|
150
|
+
if (page.length < 100)
|
|
151
|
+
break;
|
|
152
|
+
offset += page.length - ids.length;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
46
155
|
/** A verification value that is an authorization code, with its user and client; else undefined. */
|
|
47
156
|
function authCode(text) {
|
|
48
157
|
let value;
|
|
@@ -93,23 +202,7 @@ export function oauthConsents(auth) {
|
|
|
93
202
|
const consent = await db.findOne({ model: "oauthConsent", where: [pair("id", consentId), pair("userId", userId)] });
|
|
94
203
|
if (!consent)
|
|
95
204
|
return false;
|
|
96
|
-
|
|
97
|
-
// Delete first: even if cleanup fails, every issued JWT fails the live-grant check immediately.
|
|
98
|
-
await db.deleteMany({ model: "oauthConsent", where });
|
|
99
|
-
for (const model of ["oauthRefreshToken", "oauthAccessToken"])
|
|
100
|
-
await db.updateMany({ model, where: [...where, { field: "revoked", value: null }], update: { revoked: new Date() } });
|
|
101
|
-
// An unredeemed code would mint a new token lineage after reconsent.
|
|
102
|
-
const code = (v) => { const x = authCode(v); return x?.userId === userId && x.query.client_id === consent.clientId; };
|
|
103
|
-
const codes = [{ field: "value", operator: "contains", value: JSON.stringify(userId) }, { field: "value", operator: "contains", value: JSON.stringify(consent.clientId) }];
|
|
104
|
-
for (let offset = 0;;) {
|
|
105
|
-
const page = await db.findMany({ model: "verification", where: codes, limit: 100, offset, sortBy: { field: "id", direction: "asc" } });
|
|
106
|
-
const ids = page.filter(v => code(v.value)).map(v => v.id);
|
|
107
|
-
if (ids.length)
|
|
108
|
-
await db.deleteMany({ model: "verification", where: [{ field: "id", operator: "in", value: ids }] });
|
|
109
|
-
if (page.length < 100)
|
|
110
|
-
break;
|
|
111
|
-
offset += page.length - ids.length;
|
|
112
|
-
}
|
|
205
|
+
await endGrants(db, userId, consent.clientId);
|
|
113
206
|
return true;
|
|
114
207
|
},
|
|
115
208
|
};
|
package/dist/auth/routes.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Optional app-mounted auth routes; consent and raw admin routes resolve fresh authority. Better Auth owns tokens. */
|
|
2
2
|
import { checkOrigin, surfaceFailure, ContractError } from "../kernel/contracts.js";
|
|
3
|
+
import { draining } from "../kernel/drain.js";
|
|
3
4
|
import { guardAuthAdminRequest } from "./admin.js";
|
|
4
5
|
import { resolveCaller } from "./caller.js";
|
|
5
6
|
const PRIVATE = { "cache-control": "private, no-store" };
|
|
@@ -17,7 +18,7 @@ export function authRoutes(loader, options) {
|
|
|
17
18
|
if (options.connectedAppsPage && (!options.connectedAppsPage.startsWith("/") || options.connectedAppsPage.startsWith("//") || /[\\\r\n]/.test(options.connectedAppsPage)))
|
|
18
19
|
throw new Error("connectedAppsPage must be a same-origin absolute path.");
|
|
19
20
|
const resolve = options.resolver ?? ((request) => resolveCaller(request, loader));
|
|
20
|
-
return async (request) => {
|
|
21
|
+
return draining(async (request) => {
|
|
21
22
|
const { pathname } = new URL(request.url);
|
|
22
23
|
const route = `${request.method} ${pathname}`;
|
|
23
24
|
if (pathname === `${base}/methods` && request.method === "GET")
|
|
@@ -47,7 +48,7 @@ export function authRoutes(loader, options) {
|
|
|
47
48
|
catch (error) {
|
|
48
49
|
return surfaceFailure(error, undefined, `auth ${route}`);
|
|
49
50
|
}
|
|
50
|
-
};
|
|
51
|
+
});
|
|
51
52
|
}
|
|
52
53
|
function consentSurface(auth, options) {
|
|
53
54
|
const { listOAuthConsents: list, revokeOAuthConsent: revoke } = auth;
|
package/dist/build/compiler.d.ts
CHANGED
|
@@ -1,119 +1,3 @@
|
|
|
1
|
-
export interface AdminNavGroup {
|
|
2
|
-
readonly id: string;
|
|
3
|
-
readonly title: LocalizedText;
|
|
4
|
-
readonly order?: number;
|
|
5
|
-
}
|
|
6
|
-
export type LocalizedText = string | Readonly<Record<string, string>>;
|
|
7
|
-
export interface AdminExtension {
|
|
8
|
-
readonly apiVersion?: 1;
|
|
9
|
-
readonly id: string;
|
|
10
|
-
readonly title?: LocalizedText;
|
|
11
|
-
readonly module?: string;
|
|
12
|
-
readonly integrity?: string;
|
|
13
|
-
readonly source?: unknown;
|
|
14
|
-
readonly contributes: AdminExtensionContributions;
|
|
15
|
-
}
|
|
16
|
-
export interface AdminExtensionContributions {
|
|
17
|
-
readonly pages?: readonly AdminExtensionPage[];
|
|
18
|
-
readonly settings?: readonly AdminExtensionSettings[];
|
|
19
|
-
readonly actions?: readonly AdminExtensionAction[];
|
|
20
|
-
readonly panels?: readonly AdminExtensionPanel[];
|
|
21
|
-
readonly fields?: readonly AdminExtensionField[];
|
|
22
|
-
}
|
|
23
|
-
export interface AdminExtensionField {
|
|
24
|
-
readonly id: string;
|
|
25
|
-
readonly target: AdminFieldTarget;
|
|
26
|
-
readonly role?: AdminRole;
|
|
27
|
-
readonly when?: AdminExtensionWhen;
|
|
28
|
-
readonly optionsSchema?: JsonSchema;
|
|
29
|
-
}
|
|
30
|
-
export type JsonSchema = {
|
|
31
|
-
readonly $defs?: Readonly<Record<string, JsonSchema>>;
|
|
32
|
-
readonly $ref?: string;
|
|
33
|
-
readonly oneOf?: readonly JsonSchema[];
|
|
34
|
-
readonly const?: unknown;
|
|
35
|
-
readonly type?: string | readonly string[];
|
|
36
|
-
readonly properties?: Readonly<Record<string, JsonSchema>>;
|
|
37
|
-
readonly required?: readonly string[];
|
|
38
|
-
readonly items?: JsonSchema;
|
|
39
|
-
readonly enum?: readonly unknown[];
|
|
40
|
-
readonly format?: string;
|
|
41
|
-
readonly pattern?: string;
|
|
42
|
-
readonly minLength?: number;
|
|
43
|
-
readonly maxLength?: number;
|
|
44
|
-
readonly minimum?: number;
|
|
45
|
-
readonly maximum?: number;
|
|
46
|
-
readonly minItems?: number;
|
|
47
|
-
readonly maxItems?: number;
|
|
48
|
-
readonly nullable?: boolean;
|
|
49
|
-
readonly readOnly?: boolean;
|
|
50
|
-
readonly default?: unknown;
|
|
51
|
-
readonly additionalProperties?: boolean | JsonSchema;
|
|
52
|
-
readonly description?: LocalizedText;
|
|
53
|
-
readonly title?: LocalizedText;
|
|
54
|
-
readonly "x-mantle-ref"?: string | MantleRefTarget;
|
|
55
|
-
readonly "x-mcp-hint"?: string;
|
|
56
|
-
readonly [key: string]: unknown;
|
|
57
|
-
};
|
|
58
|
-
export interface MantleRefTarget {
|
|
59
|
-
readonly schema: string;
|
|
60
|
-
readonly field: string;
|
|
61
|
-
}
|
|
62
|
-
export interface AdminExtensionWhen {
|
|
63
|
-
readonly schema?: readonly string[];
|
|
64
|
-
readonly field?: readonly string[];
|
|
65
|
-
readonly format?: readonly string[];
|
|
66
|
-
}
|
|
67
|
-
export type AdminRole = string;
|
|
68
|
-
export type AdminFieldTarget = (typeof ADMIN_EXTENSION_TARGETS.field)[number];
|
|
69
|
-
export declare const ADMIN_EXTENSION_TARGETS: {
|
|
70
|
-
readonly action: readonly [
|
|
71
|
-
"record/v1",
|
|
72
|
-
"list.selection/v1",
|
|
73
|
-
"list.toolbar/v1"
|
|
74
|
-
];
|
|
75
|
-
readonly panel: readonly [
|
|
76
|
-
"record.sidebar/v1",
|
|
77
|
-
"home/v1"
|
|
78
|
-
];
|
|
79
|
-
readonly field: readonly [
|
|
80
|
-
"field.input/v1",
|
|
81
|
-
"field.cell/v1"
|
|
82
|
-
];
|
|
83
|
-
};
|
|
84
|
-
export interface AdminExtensionPanel {
|
|
85
|
-
readonly id: string;
|
|
86
|
-
readonly title: LocalizedText;
|
|
87
|
-
readonly role: AdminRole;
|
|
88
|
-
readonly target: AdminPanelTarget;
|
|
89
|
-
readonly when?: AdminExtensionWhen;
|
|
90
|
-
}
|
|
91
|
-
export type AdminPanelTarget = (typeof ADMIN_EXTENSION_TARGETS.panel)[number];
|
|
92
|
-
export interface AdminExtensionAction {
|
|
93
|
-
readonly id: string;
|
|
94
|
-
readonly title: LocalizedText;
|
|
95
|
-
readonly role: AdminRole;
|
|
96
|
-
readonly target: AdminActionTarget;
|
|
97
|
-
readonly presentation: "run" | "confirm" | "dialog";
|
|
98
|
-
readonly destructive?: boolean;
|
|
99
|
-
readonly when?: AdminExtensionWhen;
|
|
100
|
-
}
|
|
101
|
-
export type AdminActionTarget = (typeof ADMIN_EXTENSION_TARGETS.action)[number];
|
|
102
|
-
export interface AdminExtensionSettings {
|
|
103
|
-
readonly id: string;
|
|
104
|
-
readonly title: LocalizedText;
|
|
105
|
-
readonly role: AdminRole;
|
|
106
|
-
readonly schema: JsonSchema;
|
|
107
|
-
}
|
|
108
|
-
export interface AdminExtensionPage {
|
|
109
|
-
readonly id: string;
|
|
110
|
-
readonly title: LocalizedText;
|
|
111
|
-
readonly role: AdminRole;
|
|
112
|
-
readonly nav?: {
|
|
113
|
-
readonly group: string;
|
|
114
|
-
readonly order?: number;
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
1
|
export interface SiteDefaults {
|
|
118
2
|
readonly locales?: ReadonlyArray<string>;
|
|
119
3
|
readonly brand?: string;
|
|
@@ -293,10 +177,6 @@ export declare const DIAGNOSTIC_CODES: readonly [
|
|
|
293
177
|
"SCHEMA_SEARCH_FIELD_UNKNOWN",
|
|
294
178
|
"SCHEMA_TTL_INVALID",
|
|
295
179
|
"SCHEMA_UI_INVALID",
|
|
296
|
-
"UI_EXTENSION_INVALID",
|
|
297
|
-
"UI_EXTENSION_UNKNOWN",
|
|
298
|
-
"UI_EXTENSION_TARGET",
|
|
299
|
-
"UI_EXTENSION_OPTIONS",
|
|
300
180
|
"SCHEMA_NAME_CASE_COLLISION",
|
|
301
181
|
"SCHEMA_READER_NAME_COLLISION",
|
|
302
182
|
"FIELD_NAME_CASE_COLLISION",
|
|
@@ -534,6 +414,39 @@ export interface CtxAuthScopePredicate {
|
|
|
534
414
|
}
|
|
535
415
|
export type CtxAuthPredicate = "ctx.auth";
|
|
536
416
|
export type CtxUserPredicate = "ctx.user";
|
|
417
|
+
export type JsonSchema = {
|
|
418
|
+
readonly $defs?: Readonly<Record<string, JsonSchema>>;
|
|
419
|
+
readonly $ref?: string;
|
|
420
|
+
readonly oneOf?: readonly JsonSchema[];
|
|
421
|
+
readonly const?: unknown;
|
|
422
|
+
readonly type?: string | readonly string[];
|
|
423
|
+
readonly properties?: Readonly<Record<string, JsonSchema>>;
|
|
424
|
+
readonly required?: readonly string[];
|
|
425
|
+
readonly items?: JsonSchema;
|
|
426
|
+
readonly enum?: readonly unknown[];
|
|
427
|
+
readonly format?: string;
|
|
428
|
+
readonly pattern?: string;
|
|
429
|
+
readonly minLength?: number;
|
|
430
|
+
readonly maxLength?: number;
|
|
431
|
+
readonly minimum?: number;
|
|
432
|
+
readonly maximum?: number;
|
|
433
|
+
readonly minItems?: number;
|
|
434
|
+
readonly maxItems?: number;
|
|
435
|
+
readonly nullable?: boolean;
|
|
436
|
+
readonly readOnly?: boolean;
|
|
437
|
+
readonly default?: unknown;
|
|
438
|
+
readonly additionalProperties?: boolean | JsonSchema;
|
|
439
|
+
readonly description?: LocalizedText;
|
|
440
|
+
readonly title?: LocalizedText;
|
|
441
|
+
readonly "x-mantle-ref"?: string | MantleRefTarget;
|
|
442
|
+
readonly "x-mcp-hint"?: string;
|
|
443
|
+
readonly [key: string]: unknown;
|
|
444
|
+
};
|
|
445
|
+
export interface MantleRefTarget {
|
|
446
|
+
readonly schema: string;
|
|
447
|
+
readonly field: string;
|
|
448
|
+
}
|
|
449
|
+
export type LocalizedText = string | Readonly<Record<string, string>>;
|
|
537
450
|
export type PlanRestrict = Readonly<Record<string, {
|
|
538
451
|
readonly owner?: string;
|
|
539
452
|
readonly terms: readonly UserScope[];
|
|
@@ -586,6 +499,13 @@ export interface UserFieldSpec {
|
|
|
586
499
|
export declare const RESERVED_PROCEDURE_INPUT_NAMES: readonly [
|
|
587
500
|
"expectedVersion"
|
|
588
501
|
];
|
|
502
|
+
export declare const INPUT_RULE_KEYWORDS: readonly [
|
|
503
|
+
"dependentRequired",
|
|
504
|
+
"anyOf",
|
|
505
|
+
"if",
|
|
506
|
+
"then",
|
|
507
|
+
"else"
|
|
508
|
+
];
|
|
589
509
|
export declare const EXPECTED_VERSION_PROPERTY: "expectedVersion";
|
|
590
510
|
export declare const RESERVED_ENTRY_COLUMNS: readonly [
|
|
591
511
|
"id",
|
|
@@ -680,6 +600,7 @@ export interface TranslatesBinding {
|
|
|
680
600
|
readonly on: string;
|
|
681
601
|
}
|
|
682
602
|
export type Manifest = SchemaManifest | ViewManifest | ProcedureManifest | TriggerManifest;
|
|
603
|
+
export type AdminRole = string;
|
|
683
604
|
export declare function isAdminRole(s: string): s is AdminRole;
|
|
684
605
|
export declare const PROCEDURE_MCP_ANNOTATION_KEYS: readonly [
|
|
685
606
|
"readOnlyHint",
|
|
@@ -696,6 +617,7 @@ export declare const VIEW_INPUT_RESERVED: readonly [
|
|
|
696
617
|
"cursor"
|
|
697
618
|
];
|
|
698
619
|
export declare function resolveLocalizedText(value: LocalizedText | null | undefined, preferred: string, canonical?: string | null): string | null;
|
|
620
|
+
export declare const isLocalizedText: (v: unknown) => v is LocalizedText;
|
|
699
621
|
export declare const MANTLE_BIND_KEYWORD: "x-mantle-bind";
|
|
700
622
|
export declare const MCP_HINT_KEYWORD: "x-mcp-hint";
|
|
701
623
|
export declare function resolveMantleRef(property: unknown): MantleRefTarget | null;
|
|
@@ -706,8 +628,6 @@ export declare function enumOptions(property: JsonSchema | undefined): {
|
|
|
706
628
|
export declare const MANTLE_REF_KEYWORD: "x-mantle-ref";
|
|
707
629
|
export type MediaMcpHint = "media" | "media-image" | "media-video" | "media-file";
|
|
708
630
|
export declare function isMediaMcpHint(value: unknown): value is MediaMcpHint;
|
|
709
|
-
export declare const isUiExtensionRef: (value: unknown) => value is string;
|
|
710
|
-
export declare const UI_EXTENSION_REF: RegExp;
|
|
711
631
|
export interface Diagnostic {
|
|
712
632
|
readonly code: DiagnosticCode;
|
|
713
633
|
readonly phase: Phase;
|
|
@@ -769,6 +689,7 @@ export declare const CHECK_FUNCTIONS: ReadonlyMap<string, {
|
|
|
769
689
|
}>;
|
|
770
690
|
export declare function storageColumnClash(fields: Readonly<Record<string, string>>): string | undefined;
|
|
771
691
|
export declare function notNullColumns(s: SqlSchemaDef): Set<string>;
|
|
692
|
+
export declare const NATIVE_COLUMNS: Readonly<Record<string, string>>;
|
|
772
693
|
export declare const NATIVE_NOT_NULL: ReadonlySet<string>;
|
|
773
694
|
export declare function storageColumns(s: {
|
|
774
695
|
readonly fields?: Readonly<Record<string, string>>;
|
|
@@ -787,4 +708,5 @@ export interface RowTemplate {
|
|
|
787
708
|
export declare function parseRowTemplate(text: unknown): RowTemplate | string;
|
|
788
709
|
export declare const NATIVE_OUTPUT_TYPES: Readonly<Record<string, string>>;
|
|
789
710
|
export declare function planFingerprint(plan: Omit<RuntimePlan, "fingerprint">): Promise<string>;
|
|
711
|
+
export declare const fnv: (s: string) => string;
|
|
790
712
|
export declare function canonical(v: unknown): string;
|