@protobase/server 0.21.7 → 0.21.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.
Files changed (84) hide show
  1. package/dist/aggregate-groups.js +1 -1
  2. package/dist/aggregate-service.js +3 -3
  3. package/dist/ai/ai-fields.js +9 -0
  4. package/dist/ai/ai-model.js +9 -0
  5. package/dist/ai/check-ai.d.ts +13 -0
  6. package/dist/ai/check-ai.js +11 -0
  7. package/dist/ai/fill-hook.js +47 -0
  8. package/dist/ai/fill-job.js +67 -0
  9. package/dist/ai/generate-route.js +57 -0
  10. package/dist/ai/generate-value.js +28 -0
  11. package/dist/ai/values.js +15 -0
  12. package/dist/assistant/assistant-settings.d.ts +2 -2
  13. package/dist/assistant/assistant-settings.js +7 -21
  14. package/dist/assistant/built-in-assistant.js +2 -1
  15. package/dist/assistant/model-settings.js +31 -0
  16. package/dist/assistant/turn.d.ts +1 -1
  17. package/dist/assistant/turn.js +2 -2
  18. package/dist/batch/reorder.js +2 -2
  19. package/dist/batch/run-batch.js +1 -1
  20. package/dist/better-auth/create-auth.d.ts +12 -4
  21. package/dist/better-auth/create-auth.js +0 -1
  22. package/dist/better-auth/organizations/invitation-endpoints.js +1 -1
  23. package/dist/better-auth/organizations/member-endpoints.js +1 -1
  24. package/dist/better-auth/passkey-plugin.js +1 -1
  25. package/dist/better-auth/policy-plugin.js +1 -1
  26. package/dist/better-auth/roles/roles-plugin.js +1 -1
  27. package/dist/better-auth/sign-up/sign-up-plugin.js +1 -1
  28. package/dist/better-auth/staff-sign-in-endpoints.js +1 -1
  29. package/dist/changes/capture-schema.d.ts +1 -1
  30. package/dist/changes/outbox.d.ts +1 -1
  31. package/dist/check-views.js +26 -4
  32. package/dist/comments/app-origin.js +11 -0
  33. package/dist/comments/comment-access.js +54 -0
  34. package/dist/comments/comment-input.js +26 -0
  35. package/dist/comments/comment-item.d.ts +16 -0
  36. package/dist/comments/comment-item.js +18 -0
  37. package/dist/comments/comment-users.d.ts +21 -0
  38. package/dist/comments/comment-users.js +35 -0
  39. package/dist/comments/comments-sql.d.ts +8 -0
  40. package/dist/comments/comments-sql.js +37 -0
  41. package/dist/comments/mention-email.js +37 -0
  42. package/dist/comments/mention-job.d.ts +3 -0
  43. package/dist/comments/mention-job.js +43 -0
  44. package/dist/comments/mentionable.js +38 -0
  45. package/dist/comments/post-comment.js +57 -0
  46. package/dist/comments/read-comments.js +45 -0
  47. package/dist/comments/record-comments.js +53 -0
  48. package/dist/comments/record-label.js +19 -0
  49. package/dist/create-admin.d.ts +7 -4
  50. package/dist/create-admin.js +43 -8
  51. package/dist/deps.d.ts +4 -2
  52. package/dist/error-handler.js +1 -1
  53. package/dist/files/attach.js +1 -1
  54. package/dist/files/cleanup-runner.d.ts +1 -1
  55. package/dist/files/runtime.d.ts +2 -2
  56. package/dist/files/upload.js +2 -2
  57. package/dist/history/read-history.js +1 -1
  58. package/dist/index.d.ts +7 -3
  59. package/dist/index.js +11 -9
  60. package/dist/jobs/runtime.js +1 -1
  61. package/dist/list-photos.js +3 -3
  62. package/dist/list-service.js +2 -2
  63. package/dist/map-service.js +1 -1
  64. package/dist/meta.js +2 -2
  65. package/dist/openapi/resource-paths.js +151 -0
  66. package/dist/project-config.d.ts +1 -1
  67. package/dist/read-service.js +2 -2
  68. package/dist/related-labels.js +2 -2
  69. package/dist/request-access.d.ts +2 -2
  70. package/dist/routes/record.js +17 -0
  71. package/dist/row-permissions.js +3 -3
  72. package/dist/scan-guard.d.ts +2 -2
  73. package/dist/sql-parts.js +1 -1
  74. package/dist/types.d.ts +3 -1
  75. package/dist/view-access.js +33 -5
  76. package/dist/view-related.js +15 -2
  77. package/dist/webhooks/subscription.d.ts +1 -1
  78. package/dist/webhooks/table/schema.js +1 -1
  79. package/dist/webhooks/table/webhook-table.js +1 -1
  80. package/dist/webhooks/webhooks.js +1 -1
  81. package/dist/write-pipeline.js +2 -2
  82. package/dist/write-shared.js +3 -2
  83. package/dist/write-update.js +1 -1
  84. package/package.json +4 -4
@@ -3,8 +3,8 @@ import { readTransaction } from "./transactions.js";
3
3
  import { parseParams } from "./params.js";
4
4
  import { labelRequests, relatedLabels } from "./related-labels.js";
5
5
  import { scoped, seriesRange } from "./aggregate-service.js";
6
- import { groupTotals, series } from "@protobase/query";
7
6
  import { z } from "zod";
7
+ import { groupTotals, series } from "@protobase/query";
8
8
  //#region src/aggregate-groups.ts
9
9
  const name = z.string().min(1);
10
10
  const aggregateParams = z.object({
@@ -1,13 +1,13 @@
1
1
  import { badRequest } from "./problem.js";
2
- import { columnId, filterCondition, scopeConditions, tableId, whereClause } from "./sql-parts.js";
3
2
  import { checkedFilter } from "./filter-input.js";
4
3
  import { readScope, requestAccess, requireOperation } from "./request-access.js";
4
+ import { columnId, filterCondition, scopeConditions, tableId, whereClause } from "./sql-parts.js";
5
5
  import { readTransaction } from "./transactions.js";
6
6
  import { parseParams } from "./params.js";
7
7
  import { labelRequests, relatedLabels } from "./related-labels.js";
8
- import { facetCounts, histogram, series, withoutField } from "@protobase/query";
9
- import { sql } from "kysely";
10
8
  import { z } from "zod";
9
+ import { sql } from "kysely";
10
+ import { facetCounts, histogram, series, withoutField } from "@protobase/query";
11
11
  //#region src/aggregate-service.ts
12
12
  const field = z.string().min(1);
13
13
  const filter = z.string().optional();
@@ -0,0 +1,9 @@
1
+ //#region src/ai/ai-fields.ts
2
+ /** The fields of a resource a job fills from a prompt (`.ai()`). */
3
+ const aiFields = (model) => Object.values(model.fields).filter((field) => field.ai);
4
+ /** Whether a resource has a field a job fills from a prompt. */
5
+ const hasAiFields = (models) => models.some((model) => aiFields(model).length > 0);
6
+ /** Whether a view has an action that has the model write a field (`a.ai`). */
7
+ const hasAiActions = (views) => views.some((view) => view.actions.some((action) => action.run?.kind === "ai"));
8
+ //#endregion
9
+ export { aiFields, hasAiActions, hasAiFields };
@@ -0,0 +1,9 @@
1
+ import { modelSettings } from "../assistant/model-settings.js";
2
+ //#region src/ai/ai-model.ts
3
+ /**
4
+ * The model AI fields and actions call: the assistant's, also when `url` names a chat backend elsewhere (it replaces the
5
+ * chat, not the model) and when the option is `false` (which turns off the chat only).
6
+ */
7
+ const aiModel = (option, env) => modelSettings(option === false ? void 0 : option, env);
8
+ //#endregion
9
+ export { aiModel };
@@ -0,0 +1,13 @@
1
+ import { AssistantOptions } from "../assistant/assistant-settings.js";
2
+ import { ResourceModel, ViewModel } from "@protobase/schema";
3
+ //#region src/ai/check-ai.d.ts
4
+ export type CheckAiInput = {
5
+ models: readonly ResourceModel[];
6
+ views: readonly ViewModel[];
7
+ /** The project's `options.assistant`, whose model settings AI fields and actions use. */
8
+ assistant: AssistantOptions | false | undefined;
9
+ env: Record<string, string | undefined>;
10
+ };
11
+ /** Fails when a resource has an AI field or a view an AI action and no model is set to fill or run them. */
12
+ export declare const checkAi: ({ models, views, assistant, env }: CheckAiInput) => void;
13
+ //#endregion
@@ -0,0 +1,11 @@
1
+ import { aiModel } from "./ai-model.js";
2
+ import { hasAiActions, hasAiFields } from "./ai-fields.js";
3
+ //#region src/ai/check-ai.ts
4
+ /** Fails when a resource has an AI field or a view an AI action and no model is set to fill or run them. */
5
+ const checkAi = ({ models, views, assistant, env }) => {
6
+ if (!hasAiFields(models) && !hasAiActions(views)) return;
7
+ if (aiModel(assistant, env)) return;
8
+ throw new Error("AI fields and actions need a model: set PROTOBASE_ASSISTANT_API_KEY (and PROTOBASE_ASSISTANT_MODEL when PROTOBASE_ASSISTANT_BASE_URL is not OpenRouter), or options.assistant.apiKey");
9
+ };
10
+ //#endregion
11
+ export { checkAi };
@@ -0,0 +1,47 @@
1
+ import { aiFields } from "./ai-fields.js";
2
+ import { requestAccess, writableFields } from "../request-access.js";
3
+ import { isEmpty, same, writerSession } from "./values.js";
4
+ import { encodeKey, promptFields } from "@protobase/schema/internal";
5
+ //#region src/ai/fill-hook.ts
6
+ const wantsFill = (event, field, after) => {
7
+ const reads = promptFields(field.ai.prompt);
8
+ if (event.operation === "create") return isEmpty(after[field.name]) && reads.some((name) => !isEmpty(after[name]));
9
+ if (event.operation !== "update" || !event.before) return false;
10
+ const before = event.before;
11
+ return reads.some((name) => !same(before[name], after[name])) && same(before[field.name], after[field.name]);
12
+ };
13
+ /**
14
+ * The write hook that enqueues the `ai.fill` job of each AI field a write calls for, in the write's transaction, one
15
+ * waiting job per record and field with the latest writer. The job writes as the writer, so a visitor without a token
16
+ * (a public form) or a writer who may not read what the prompt reads or update the field gets no fill.
17
+ */
18
+ const fillHook = (depsOf, job) => async (event, trx) => {
19
+ const { after } = event;
20
+ if (!after || event.anonymous) return;
21
+ const wanted = aiFields(event.resource).filter((field) => wantsFill(event, field, after));
22
+ if (wanted.length === 0) return;
23
+ const deps = depsOf();
24
+ const entry = deps.registry.find(event.resource.name);
25
+ const access = await requestAccess(deps, entry, writerSession(event.user, event.tenant));
26
+ const updatable = access.resolved.operations.update ? writableFields(access, "update") : [];
27
+ const key = encodeKey(event.resource.primaryKey.map((name) => after[name]));
28
+ for (const field of wanted) {
29
+ if (!(updatable.includes(field.name) && promptFields(field.ai.prompt).every((name) => Object.hasOwn(access.model.fields, name)))) continue;
30
+ const payload = {
31
+ resource: event.resource.name,
32
+ key,
33
+ field: field.name,
34
+ user: {
35
+ id: event.user.id,
36
+ roles: [...event.user.roles]
37
+ },
38
+ ...event.tenant !== void 0 && { tenant: event.tenant }
39
+ };
40
+ await job.enqueue(trx, payload, {
41
+ key: `ai:${event.resource.name}:${key}:${field.name}`,
42
+ mode: "replace"
43
+ });
44
+ }
45
+ };
46
+ //#endregion
47
+ export { fillHook };
@@ -0,0 +1,67 @@
1
+ import { HttpProblem } from "../problem.js";
2
+ import { same, writerSession } from "./values.js";
3
+ import { assistantRecords } from "../assistant/records.js";
4
+ import { defineJob } from "../jobs/define-job.js";
5
+ import { FatalJobError } from "../jobs/run-job.js";
6
+ import { generateValue } from "./generate-value.js";
7
+ import { promptFields } from "@protobase/schema/internal";
8
+ import { z } from "zod";
9
+ //#region src/ai/fill-job.ts
10
+ const payload = z.object({
11
+ resource: z.string(),
12
+ key: z.string(),
13
+ field: z.string(),
14
+ user: z.object({
15
+ id: z.union([z.string(), z.number()]),
16
+ roles: z.array(z.string())
17
+ }),
18
+ tenant: z.union([z.string(), z.number()]).optional()
19
+ });
20
+ const current = async (read) => {
21
+ try {
22
+ return await read;
23
+ } catch (error) {
24
+ if (error instanceof HttpProblem && error.status === 404) return void 0;
25
+ if (error instanceof HttpProblem && error.status === 403) throw new FatalJobError(error.message);
26
+ throw error;
27
+ }
28
+ };
29
+ /**
30
+ * The job that fills an AI field (`.ai()`) from its prompt, as the user whose write enqueued it. It writes nothing when
31
+ * the record is gone, when a field the prompt reads changed while the model answered (that change enqueued a newer job),
32
+ * or when the value is the one stored; when every field the prompt reads is empty the field is cleared. A model that
33
+ * fails, or a record changed between the read and the write, is retried.
34
+ */
35
+ const fillJob = (depsOf) => defineJob({
36
+ name: "ai.fill",
37
+ payload,
38
+ maxAttempts: 5,
39
+ retryDelay: "30 seconds",
40
+ concurrency: 4,
41
+ run: async (job, { signal }) => {
42
+ const deps = depsOf();
43
+ const field = deps.registry.find(job.resource)?.model.fields[job.field];
44
+ if (!field?.ai) throw new FatalJobError(`${job.resource} has no AI field "${job.field}"`);
45
+ const model = deps.ai?.model;
46
+ if (!model) throw new Error("AI fields need a model: set PROTOBASE_ASSISTANT_API_KEY");
47
+ const records = assistantRecords(deps, writerSession(job.user, job.tenant));
48
+ const read = await current(records.get(job.resource, job.key));
49
+ if (!read) return;
50
+ const value = await generateValue(model, {
51
+ prompt: field.ai.prompt,
52
+ record: read.record,
53
+ field
54
+ }, signal);
55
+ const now = await current(records.get(job.resource, job.key));
56
+ if (!now || promptFields(field.ai.prompt).some((name) => !same(now.record[name], read.record[name]))) return;
57
+ if (same(now.record[job.field] ?? null, value)) return;
58
+ try {
59
+ await records.update(job.resource, job.key, { [job.field]: value }, { etag: now.etag });
60
+ } catch (error) {
61
+ if (error instanceof HttpProblem && error.status === 403) throw new FatalJobError(error.message);
62
+ throw error;
63
+ }
64
+ }
65
+ });
66
+ //#endregion
67
+ export { fillJob };
@@ -0,0 +1,57 @@
1
+ import { HttpProblem, badRequest, forbidden, notFound } from "../problem.js";
2
+ import { readRecord } from "../read-service.js";
3
+ import { runWrite } from "../write-pipeline.js";
4
+ import { ModelError } from "../assistant/chat-completions.js";
5
+ import { generateValue } from "./generate-value.js";
6
+ import { promptFields } from "@protobase/schema/internal";
7
+ import { pickView } from "@protobase/schema";
8
+ //#region src/ai/generate-route.ts
9
+ /** How long the model may take to answer one click. */
10
+ const answerTimeoutMs = 6e4;
11
+ const actionName = (body) => {
12
+ const action = typeof body === "object" && body !== null ? body.action : void 0;
13
+ if (typeof action !== "string" || action === "") throw badRequest("invalid-action", "Send the name of the AI action, as { \"action\": \"<name>\" }");
14
+ return action;
15
+ };
16
+ const aiRun = (deps, entry, session, name) => {
17
+ const run = pickView(deps.views, session.user.roles, entry.name)?.actions.find((action) => action.name === name)?.run;
18
+ if (run?.kind !== "ai" || run.prompt === void 0) throw notFound("There is no such AI action");
19
+ return {
20
+ prompt: run.prompt,
21
+ writes: run.writes
22
+ };
23
+ };
24
+ const unavailable = () => new HttpProblem(502, "ai-unavailable", "Bad Gateway", "The model did not answer. Try again later.");
25
+ /**
26
+ * Runs the AI action `body.action` on a record as the caller (`POST /{resource}/{key}:generate`): reads the record,
27
+ * has the model write the action's field from its prompt, and updates it with the ETag it read, so an edit made while
28
+ * the model answered fails the write with 412 instead of being overwritten. Only a signed-in user whose view has the
29
+ * action, who may read the fields its prompt names and update the field it writes, runs it; nothing is asked of the
30
+ * model before those checks pass. A model that fails is reported to the app and answers 502, without its message.
31
+ */
32
+ const generateField = async (deps, entry, session, key, body, signal) => {
33
+ if (session.anonymous) throw forbidden("ai-needs-user", "Sign in to run an AI action");
34
+ const run = aiRun(deps, entry, session, actionName(body));
35
+ const model = deps.ai?.model;
36
+ if (!model) throw new HttpProblem(503, "ai-not-configured", "Service Unavailable", "AI actions need a model: set PROTOBASE_ASSISTANT_API_KEY");
37
+ const { record, etag, permissions } = await readRecord(deps, entry, session, key);
38
+ if (!promptFields(run.prompt).every((name) => Object.hasOwn(record, name)) || permissions.fields[run.writes] !== "edit") throw forbidden("ai-forbidden", `You may not run this AI action on this ${entry.name} record`);
39
+ const value = await generateValue(model, {
40
+ prompt: run.prompt,
41
+ record,
42
+ field: entry.model.fields[run.writes]
43
+ }, AbortSignal.any([signal, AbortSignal.timeout(answerTimeoutMs)])).catch((error) => {
44
+ if (!(error instanceof ModelError) && !(error instanceof Error && error.name === "TimeoutError")) throw error;
45
+ deps.ai?.report?.(error);
46
+ throw unavailable();
47
+ });
48
+ if (value === null) throw new HttpProblem(422, "ai-no-value", "Unprocessable Content", "The fields the prompt reads are empty, or the model gave no answer");
49
+ return runWrite(deps, entry, session, {
50
+ operation: "update",
51
+ key,
52
+ body: { [run.writes]: value },
53
+ ifMatch: etag
54
+ });
55
+ };
56
+ //#endregion
57
+ export { generateField };
@@ -0,0 +1,28 @@
1
+ import "../assistant/model-settings.js";
2
+ import { isEmpty } from "./values.js";
3
+ import { streamChatCompletion } from "../assistant/chat-completions.js";
4
+ import { promptFields, renderPrompt } from "@protobase/schema/internal";
5
+ //#region src/ai/generate-value.ts
6
+ const instruction = "You write the value of one field of a record in a business app. Answer with the value only: no introduction, no quotes, no Markdown code fences.";
7
+ /**
8
+ * The value the model writes for `field` from `prompt` filled with the record's values, trimmed; `null` when every field
9
+ * the prompt reads is empty, without asking the model, or when the model answers nothing. Rejects with a `ModelError`.
10
+ */
11
+ const generateValue = async (settings, { prompt, record, field }, signal) => {
12
+ if (promptFields(prompt).every((name) => isEmpty(record[name]))) return null;
13
+ const choices = field.type === "enum" && field.enumValues ? `\n\nAnswer with exactly one of: ${field.enumValues.join(", ")}` : "";
14
+ const messages = [{
15
+ role: "system",
16
+ content: instruction
17
+ }, {
18
+ role: "user",
19
+ content: `${renderPrompt(prompt, record)}${choices}`
20
+ }];
21
+ const { content } = await streamChatCompletion(settings, messages, () => {}, {
22
+ ...signal && { signal },
23
+ ...settings.baseUrl === "https://openrouter.ai/api/v1" && { reasoning: "low" }
24
+ });
25
+ return content.trim() || null;
26
+ };
27
+ //#endregion
28
+ export { generateValue };
@@ -0,0 +1,15 @@
1
+ //#region src/ai/values.ts
2
+ /** Whether a field holds nothing the model could read: `null`, missing or blank text. */
3
+ const isEmpty = (value) => value === null || value === void 0 || typeof value === "string" && value.trim() === "";
4
+ /** Whether two values of a record are the same, json and file values by their content. */
5
+ const same = (a, b) => a === b || typeof a === "object" && typeof b === "object" && JSON.stringify(a) === JSON.stringify(b);
6
+ /** The session of a writer as a write hook saw them, in their tenant: what an AI field is filled as. */
7
+ const writerSession = (user, tenant) => ({
8
+ user,
9
+ ...tenant !== void 0 && {
10
+ tenant,
11
+ organization: { id: tenant }
12
+ }
13
+ });
14
+ //#endregion
15
+ export { isEmpty, same, writerSession };
@@ -26,11 +26,11 @@ export type AssistantSettings = {
26
26
  chats: string;
27
27
  };
28
28
  export declare const assistantVariables: {
29
- readonly url: "PROTOBASE_ASSISTANT_URL";
29
+ readonly chats: "PROTOBASE_ASSISTANT_CHATS";
30
30
  readonly apiKey: "PROTOBASE_ASSISTANT_API_KEY";
31
31
  readonly model: "PROTOBASE_ASSISTANT_MODEL";
32
32
  readonly baseUrl: "PROTOBASE_ASSISTANT_BASE_URL";
33
- readonly chats: "PROTOBASE_ASSISTANT_CHATS";
33
+ readonly url: "PROTOBASE_ASSISTANT_URL";
34
34
  };
35
35
  /** The roles that see the assistant: `/meta` names it, and the built-in one answers, only for a caller with one of them. */
36
36
  export declare const assistantRoles: readonly ["admin", "ai"];
@@ -1,16 +1,13 @@
1
1
  import { checkedUrl } from "../checked-url.js";
2
+ import { modelSettings, modelVariables } from "./model-settings.js";
2
3
  //#region src/assistant/assistant-settings.ts
3
4
  const assistantVariables = {
4
5
  url: "PROTOBASE_ASSISTANT_URL",
5
- apiKey: "PROTOBASE_ASSISTANT_API_KEY",
6
- model: "PROTOBASE_ASSISTANT_MODEL",
7
- baseUrl: "PROTOBASE_ASSISTANT_BASE_URL",
6
+ ...modelVariables,
8
7
  chats: "PROTOBASE_ASSISTANT_CHATS"
9
8
  };
10
9
  /** Where the built-in assistant keeps conversations without `chats`: relative to the working directory. */
11
10
  const defaultChatsDirectory = ".protobase/chats";
12
- const openRouterUrl = "https://openrouter.ai/api/v1";
13
- const openRouterModel = "openrouter/auto";
14
11
  /** The roles that see the assistant: `/meta` names it, and the built-in one answers, only for a caller with one of them. */
15
12
  const assistantRoles = ["admin", "ai"];
16
13
  const seesAssistant = (roles) => assistantRoles.some((role) => roles.includes(role));
@@ -21,30 +18,19 @@ const seesAssistant = (roles) => assistantRoles.some((role) => roles.includes(ro
21
18
  */
22
19
  const assistantSettings = (option, env) => {
23
20
  if (option === false) return void 0;
24
- const value = (key) => {
25
- return (option?.[key] ?? env[assistantVariables[key]])?.trim() || void 0;
26
- };
21
+ const value = (key) => (option?.[key] ?? env[assistantVariables[key]])?.trim() || void 0;
27
22
  const source = (key) => option?.[key] === void 0 ? assistantVariables[key] : `options.assistant.${key}`;
28
23
  const url = value("url");
29
24
  if (url) return {
30
25
  kind: "external",
31
26
  url: checkedUrl(url, source("url"))
32
27
  };
33
- const apiKey = value("apiKey");
34
- if (!apiKey) return void 0;
35
- const baseUrl = checkedUrl(value("baseUrl") ?? "https://openrouter.ai/api/v1", source("baseUrl"));
36
- const model = value("model") ?? (baseUrl === "https://openrouter.ai/api/v1" ? "openrouter/auto" : void 0);
37
- if (!model) throw new Error(`${assistantVariables.model} (or options.assistant.model) is required with an endpoint other than OpenRouter's`);
38
- return {
28
+ const model = modelSettings(option, env);
29
+ return model && {
39
30
  kind: "built-in",
40
- model: {
41
- baseUrl,
42
- apiKey,
43
- model,
44
- ...option && option.fetch && { fetch: option.fetch }
45
- },
31
+ model,
46
32
  chats: value("chats") ?? ".protobase/chats"
47
33
  };
48
34
  };
49
35
  //#endregion
50
- export { assistantRoles, assistantSettings, assistantVariables, defaultChatsDirectory, openRouterModel, openRouterUrl, seesAssistant };
36
+ export { assistantRoles, assistantSettings, assistantVariables, defaultChatsDirectory, seesAssistant };
@@ -1,4 +1,5 @@
1
- import { openRouterUrl, seesAssistant } from "./assistant-settings.js";
1
+ import { openRouterUrl } from "./model-settings.js";
2
+ import { seesAssistant } from "./assistant-settings.js";
2
3
  import { appContext } from "./app-context.js";
3
4
  import { conversationKey, createConversations } from "./conversations.js";
4
5
  import { fileConversationStore } from "./file-conversation-store.js";
@@ -0,0 +1,31 @@
1
+ import { checkedUrl } from "../checked-url.js";
2
+ //#region src/assistant/model-settings.ts
3
+ /** The environment variables of the model settings, which `options.assistant`'s keys of the same name win over. */
4
+ const modelVariables = {
5
+ apiKey: "PROTOBASE_ASSISTANT_API_KEY",
6
+ model: "PROTOBASE_ASSISTANT_MODEL",
7
+ baseUrl: "PROTOBASE_ASSISTANT_BASE_URL"
8
+ };
9
+ const openRouterUrl = "https://openrouter.ai/api/v1";
10
+ const openRouterModel = "openrouter/auto";
11
+ /**
12
+ * The chat model an API key gives, the option over its environment variable: OpenRouter unless `baseUrl` names another
13
+ * OpenAI-compatible endpoint, which then needs a `model`; none without a key. A value that cannot work throws.
14
+ */
15
+ const modelSettings = (option, env) => {
16
+ const value = (key) => (option?.[key] ?? env[modelVariables[key]])?.trim() || void 0;
17
+ const source = (key) => option?.[key] === void 0 ? modelVariables[key] : `options.assistant.${key}`;
18
+ const apiKey = value("apiKey");
19
+ if (!apiKey) return void 0;
20
+ const baseUrl = checkedUrl(value("baseUrl") ?? "https://openrouter.ai/api/v1", source("baseUrl"));
21
+ const model = value("model") ?? (baseUrl === "https://openrouter.ai/api/v1" ? "openrouter/auto" : void 0);
22
+ if (!model) throw new Error(`${modelVariables.model} (or options.assistant.model) is required with an endpoint other than OpenRouter's`);
23
+ return {
24
+ baseUrl,
25
+ apiKey,
26
+ model,
27
+ ...option?.fetch && { fetch: option.fetch }
28
+ };
29
+ };
30
+ //#endregion
31
+ export { modelSettings, modelVariables, openRouterModel, openRouterUrl };
@@ -1,6 +1,6 @@
1
1
  import { Session } from "../types.js";
2
- import { ToolRecords } from "./records.js";
3
2
  import { ModelSettings, ReasoningEffort } from "./chat-completions.js";
3
+ import { ToolRecords } from "./records.js";
4
4
  import { Conversation } from "./conversations.js";
5
5
  import { AssistantTool } from "./tools.js";
6
6
  //#region src/assistant/turn.d.ts
@@ -1,7 +1,7 @@
1
+ import { noRecords } from "./records.js";
2
+ import { ModelError, streamChatCompletion } from "./chat-completions.js";
1
3
  import { requestApproval } from "./approval.js";
2
4
  import { withCacheBreakpoints } from "./cache-breakpoints.js";
3
- import { ModelError, streamChatCompletion } from "./chat-completions.js";
4
- import { noRecords } from "./records.js";
5
5
  //#region src/assistant/turn.ts
6
6
  const toolResult = async (call, tools, context) => {
7
7
  const tool = tools.find((entry) => entry.name === call.function.name);
@@ -1,8 +1,8 @@
1
1
  import { badRequest } from "../problem.js";
2
- import { columnId, keyConditions, scopeConditions, tableId, whereClause } from "../sql-parts.js";
3
- import { fetchRecord } from "../records.js";
4
2
  import { tenantScope } from "../tenant.js";
5
3
  import { requireOperation, targetFilter, writableFields } from "../request-access.js";
4
+ import { columnId, keyConditions, scopeConditions, tableId, whereClause } from "../sql-parts.js";
5
+ import { fetchRecord } from "../records.js";
6
6
  import { validateBody } from "../validate-body.js";
7
7
  import { missing, shown } from "../write-shared.js";
8
8
  import { applyUpdate, planUpdate } from "../write-update.js";
@@ -1,7 +1,7 @@
1
1
  import { HttpProblem, badRequest, notFound } from "../problem.js";
2
- import { problemOf } from "../error-handler.js";
3
2
  import { requestAccess } from "../request-access.js";
4
3
  import { writeTransaction } from "../transactions.js";
4
+ import { problemOf } from "../error-handler.js";
5
5
  import { actorSettings, wakeDispatch } from "../changes/actor.js";
6
6
  import { keyOf } from "../write-shared.js";
7
7
  import { writeIn } from "../write-pipeline.js";
@@ -36,7 +36,7 @@ export type CreateAuthOptions = {
36
36
  appName?: string;
37
37
  /** Signs cookies and encrypts the JWT private keys. Generate with `openssl rand -base64 32`. */
38
38
  secret: string;
39
- /** Extra origins that may call the auth endpoints. `baseURL` and `https://*.trycloudflare.com` are always trusted. */
39
+ /** Extra origins that may call the auth endpoints, such as the exact host of a dev tunnel. `baseURL` and `origins` are always trusted. */
40
40
  trustedOrigins?: string[];
41
41
  /**
42
42
  * The roles users can have, for example `['admin', 'accountant', 'warehouse']`. `admin` is always included; default
@@ -800,7 +800,7 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
800
800
  expiresAt: Date;
801
801
  token: string;
802
802
  ipAddress?: string | null | undefined;
803
- userAgent? /** Open sign-up is on and offered: the app asks for it, and can mail the codes. */ : string | null | undefined;
803
+ userAgent?: string | null | undefined;
804
804
  };
805
805
  };
806
806
  }>>[];
@@ -3749,7 +3749,15 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
3749
3749
  name?: string;
3750
3750
  slug?: string;
3751
3751
  logo?: string | null;
3752
- metadata?: Record<string, any>;
3752
+ metadata?
3753
+ /**
3754
+ * The options `createAuth` gives Better Auth, with the roles, default role, mailer, operator provider and platform
3755
+ * sign-in provider already resolved.
3756
+ */
3757
+ /**
3758
+ * The options `createAuth` gives Better Auth, with the roles, default role, mailer, operator provider and platform
3759
+ * sign-in provider already resolved.
3760
+ */ : Record<string, any>;
3753
3761
  [key: string]: any;
3754
3762
  };
3755
3763
  }>;
@@ -4232,7 +4240,7 @@ export declare const createAuth: (options: CreateAuthOptions) => import("better-
4232
4240
  hooks: {
4233
4241
  after: {
4234
4242
  matcher: (context: {
4235
- path? /** Sign-in attempts allowed per minute and client, default 5; also the sign-up requests allowed. */ : string;
4243
+ path?: string;
4236
4244
  }) => boolean;
4237
4245
  handler: import("better-auth").Middleware<import("better-auth").MiddlewareOptions, (inputContext: import("better-auth").MiddlewareInputContext<import("better-auth").MiddlewareOptions>) => Promise<void>>;
4238
4246
  }[];
@@ -64,7 +64,6 @@ const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, defini
64
64
  trustedOrigins: [
65
65
  options.baseURL,
66
66
  ...origins,
67
- "https://*.trycloudflare.com",
68
67
  ...options.trustedOrigins ?? []
69
68
  ],
70
69
  emailAndPassword: {
@@ -5,9 +5,9 @@ import { checkNewAccountPassword } from "../new-account-password.js";
5
5
  import { effectiveSignInPolicy } from "../sign-in-policy.js";
6
6
  import { readSignInPolicy } from "../policy-store.js";
7
7
  import { acceptInvitation, invitationOfToken } from "./accept-invitation.js";
8
+ import * as z$1 from "zod";
8
9
  import { APIError, createAuthEndpoint, createAuthMiddleware, sessionMiddleware } from "better-auth/api";
9
10
  import { setSessionCookie } from "better-auth/cookies";
10
- import * as z$1 from "zod";
11
11
  //#region src/better-auth/organizations/invitation-endpoints.ts
12
12
  const tokenBody = z$1.object({ token: z$1.string().min(1) });
13
13
  const startIn = (context, sessionToken, organizationId) => context.internalAdapter.updateSession(sessionToken, { activeOrganizationId: organizationId });
@@ -1,8 +1,8 @@
1
1
  import "./role-definitions.js";
2
2
  import { checkAppRoleChange, effectiveRoles, globalRoles } from "./app-roles.js";
3
3
  import { findMember, findMemberById, findOrganization, organizationRoleOf, setAppRoles } from "./store.js";
4
- import { APIError, createAuthEndpoint, sessionMiddleware } from "better-auth/api";
5
4
  import * as z$1 from "zod";
5
+ import { APIError, createAuthEndpoint, sessionMiddleware } from "better-auth/api";
6
6
  //#region src/better-auth/organizations/member-endpoints.ts
7
7
  const memberNotFound = {
8
8
  code: "MEMBER_NOT_FOUND",
@@ -1,7 +1,7 @@
1
1
  import { pluginNames } from "./snake-case-names.js";
2
+ import * as z$1 from "zod";
2
3
  import { APIError, createAuthEndpoint, getSessionFromCtx } from "better-auth/api";
3
4
  import { setSessionCookie } from "better-auth/cookies";
4
- import * as z$1 from "zod";
5
5
  import { PASSKEY_ERROR_CODES, passkey } from "@better-auth/passkey";
6
6
  import { generateAuthenticationOptions, verifyAuthenticationResponse } from "@simplewebauthn/server";
7
7
  import { generateRandomString } from "better-auth/crypto";
@@ -3,8 +3,8 @@ import { effectiveSignInPolicy, missingRequiredMethods, parseSignInPolicy, signI
3
3
  import { readSignInPolicy, saveSignInPolicy, signInPolicySchema } from "./policy-store.js";
4
4
  import { isPoliced, lastPasskeyRequired, policyRefusal, setupRequired } from "./sign-in-enforcement.js";
5
5
  import { adminOnly } from "./admin-only.js";
6
- import { APIError, createAuthEndpoint, createAuthMiddleware, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
7
6
  import * as z$1 from "zod";
7
+ import { APIError, createAuthEndpoint, createAuthMiddleware, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
8
8
  //#region src/better-auth/policy-plugin.ts
9
9
  const accountMethods = async (context, user, socialProviders) => {
10
10
  const accounts = await context.internalAdapter.findAccounts(user.id);
@@ -1,7 +1,7 @@
1
1
  import "../organizations/role-definitions.js";
2
2
  import { sessionRoles } from "./session-role.js";
3
- import { APIError, createAuthEndpoint, createAuthMiddleware, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
4
3
  import * as z$1 from "zod";
4
+ import { APIError, createAuthEndpoint, createAuthMiddleware, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
5
5
  //#region src/better-auth/roles/roles-plugin.ts
6
6
  /**
7
7
  * Switching the role a session acts with, as switching its organization: the session keeps it, and its tokens carry it
@@ -4,9 +4,9 @@ import { effectiveSignInPolicy } from "../sign-in-policy.js";
4
4
  import { readSignInPolicy } from "../policy-store.js";
5
5
  import { consumeSignUpCode, issueSignUpCode, signUpCodeLifetime } from "./sign-up-code.js";
6
6
  import { consumeSignUpTicket, issueSignUpTicket } from "./sign-up-ticket.js";
7
+ import * as z$1 from "zod";
7
8
  import { APIError, createAuthEndpoint } from "better-auth/api";
8
9
  import { setSessionCookie } from "better-auth/cookies";
9
- import * as z$1 from "zod";
10
10
  //#region src/better-auth/sign-up/sign-up-plugin.ts
11
11
  const address = z$1.string().trim().toLowerCase().pipe(z$1.email());
12
12
  /** The paths of the sign-up endpoints, as `ctx.path` and the rate limits name them. */
@@ -5,9 +5,9 @@ import { effectiveSignInPolicy } from "./sign-in-policy.js";
5
5
  import { readSignInPolicy } from "./policy-store.js";
6
6
  import { policyRefusal, staffSignInPath } from "./sign-in-enforcement.js";
7
7
  import { logStaffSignIn } from "./staff-log.js";
8
+ import * as z$1 from "zod";
8
9
  import { APIError, createAuthEndpoint } from "better-auth/api";
9
10
  import { expireCookie, setSessionCookie } from "better-auth/cookies";
10
- import * as z$1 from "zod";
11
11
  //#region src/better-auth/staff-sign-in-endpoints.ts
12
12
  const startLifetimeMs = 6e5;
13
13
  const stateCookie = "staff_state";
@@ -1,6 +1,6 @@
1
1
  import { EventsOptions } from "./events-options.js";
2
- import { Db } from "@protobase/query";
3
2
  import { ResourceModel } from "@protobase/schema";
3
+ import { Db } from "@protobase/query";
4
4
  //#region src/changes/capture-schema.d.ts
5
5
  /** The command that writes what the config needs as a migration file of the project. */
6
6
  export declare const migrationCommand = "protobase migration";
@@ -1,7 +1,7 @@
1
1
  import { Row } from "../types.js";
2
2
  import { CapturedRow } from "./coalesce.js";
3
- import { Db } from "@protobase/query";
4
3
  import { ResourceModel } from "@protobase/schema";
4
+ import { Db } from "@protobase/query";
5
5
  //#region src/changes/outbox.d.ts
6
6
  /**
7
7
  * A captured row's columns from the JSON text the database stores: the columns of 64-bit integer and decimal fields as