@protobase/server 0.69.0 → 0.69.2

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.
@@ -1,5 +1,6 @@
1
1
  import { UserColumn } from "./user-table.js";
2
2
  import { Mailer } from "../mail/mail-message.js";
3
+ import { Session } from "../types.js";
3
4
  import { SignInPolicy } from "./sign-in-policy.js";
4
5
  import { StaffSignIn } from "./staff-log.js";
5
6
  import { Person } from "./organizations/people-endpoint.js";
@@ -3327,6 +3328,10 @@ export declare const buildAppAuth: (settings: AuthSettings, input: AppAuthInput)
3327
3328
  scopes: string[];
3328
3329
  created_at: Date;
3329
3330
  needs_sign_in: boolean;
3331
+ used_by: {
3332
+ id: string;
3333
+ name: string;
3334
+ }[];
3330
3335
  }[];
3331
3336
  }[];
3332
3337
  }>;
@@ -9157,6 +9162,23 @@ export declare const buildAppAuth: (settings: AuthSettings, input: AppAuthInput)
9157
9162
  events: {
9158
9163
  set: (next: ConnectionListener) => undefined;
9159
9164
  };
9165
+ list: (session: Session) => Promise<{
9166
+ key: string;
9167
+ label: string;
9168
+ provider: string;
9169
+ scopes: string[];
9170
+ link_params: Record<string, string>;
9171
+ accounts: {
9172
+ id: string;
9173
+ scopes: string[];
9174
+ created_at: Date;
9175
+ needs_sign_in: boolean;
9176
+ used_by: {
9177
+ id: string;
9178
+ name: string;
9179
+ }[];
9180
+ }[];
9181
+ }[]>;
9160
9182
  };
9161
9183
  mcp: {
9162
9184
  resource: string;
@@ -7,5 +7,12 @@ export type ConnectionListener = {
7
7
  disconnected: (accountId: string) => Promise<void>;
8
8
  /** The accounts among `accountIds` that a trigger waits on to be signed in again. */
9
9
  needsSignIn: (accountIds: string[]) => Promise<string[]>;
10
+ /** The workflows among those `userId` turned on, and may still change, whose enabled version binds one of `accountIds`, by account. */
11
+ uses: (userId: string, accountIds: string[]) => Promise<AccountUses>;
10
12
  };
13
+ /** The workflows that use each account, by its id. */
14
+ export type AccountUses = Record<string, {
15
+ id: string;
16
+ name: string;
17
+ }[]>;
11
18
  //#endregion
@@ -9,7 +9,8 @@ const connectionEvents = () => {
9
9
  set: (next) => void (listener = next),
10
10
  linked: async (accountId) => listener?.linked(accountId),
11
11
  disconnected: async (accountId) => listener?.disconnected(accountId),
12
- needsSignIn: async (accountIds) => listener && accountIds.length > 0 ? listener.needsSignIn(accountIds) : []
12
+ needsSignIn: async (accountIds) => listener && accountIds.length > 0 ? listener.needsSignIn(accountIds) : [],
13
+ uses: async (userId, accountIds) => listener && accountIds.length > 0 ? listener.uses(userId, accountIds) : {}
13
14
  };
14
15
  };
15
16
  //#endregion
@@ -1,5 +1,4 @@
1
- import { storedScopes } from "../../personal-mail/linked-accounts.js";
2
- import { holdsScopes } from "../../workflows/connections/connection-provider.js";
1
+ import { listConnections } from "./list-connections.js";
3
2
  import { revokeToken } from "./revoke-token.js";
4
3
  import * as z$1 from "zod";
5
4
  import { decryptOAuthToken } from "better-auth/oauth2";
@@ -15,7 +14,8 @@ const notFound = () => new APIError("NOT_FOUND", {
15
14
  });
16
15
  /**
17
16
  * The signed-in person's connections: `GET /connections` lists each connection the app's workflow plugins declare with
18
- * the person's accounts that fit it, and whether a trigger waits for one to be signed in again, never a token.
17
+ * the person's accounts that fit it, whether a trigger waits for one to be signed in again and the workflows that use
18
+ * it, never a token (see `listConnections`).
19
19
  * `POST /connections/disconnect` revokes an account's token at its provider, then deletes it whatever the provider
20
20
  * answered, and stops the triggers on it; their own account only, and never the last account they sign in with. Connecting is
21
21
  * Better Auth's `/link-social`; a sign-in (not a link) through a provider only connections use is refused.
@@ -29,22 +29,12 @@ const connectionsPlugin = ({ connections, events, allowPrivateAddresses }) => {
29
29
  method: "GET",
30
30
  use: [sessionMiddleware]
31
31
  }, async (ctx) => {
32
- const accounts = await ctx.context.internalAdapter.findAccounts(ctx.context.session.user.id);
33
- const fitting = (connection) => accounts.filter((account) => account.providerId === connection.providerId && holdsScopes(storedScopes(account.scope), connection.declaration));
34
- const waiting = new Set(await events.needsSignIn(connections.flatMap((connection) => fitting(connection).map((account) => account.id))));
35
- return ctx.json({ connections: connections.map((connection) => ({
36
- key: connection.key,
37
- label: connection.declaration.label,
38
- provider: connection.providerId,
39
- scopes: connection.declaration.scopes ?? [],
40
- link_params: connection.declaration.link_params ?? {},
41
- accounts: fitting(connection).map((account) => ({
42
- id: account.id,
43
- scopes: storedScopes(account.scope),
44
- created_at: account.createdAt,
45
- needs_sign_in: waiting.has(account.id)
46
- }))
47
- })) });
32
+ const adapter = ctx.context.internalAdapter;
33
+ return ctx.json({ connections: await listConnections({
34
+ findAccounts: (userId) => adapter.findAccounts(userId),
35
+ connections,
36
+ events
37
+ }, ctx.context.session.user.id) });
48
38
  }),
49
39
  disconnectConnection: createAuthEndpoint(connectionPaths.disconnect, {
50
40
  method: "POST",
@@ -0,0 +1,34 @@
1
+ import { storedScopes } from "../../personal-mail/linked-accounts.js";
2
+ import { holdsScopes } from "../../workflows/connections/connection-provider.js";
3
+ //#region src/better-auth/connections/list-connections.ts
4
+ /**
5
+ * A person's connections: each connection the app's workflow plugins declare with the person's accounts that fit it,
6
+ * whether a trigger waits for one to be signed in again, and the workflows that use it. Each account's fields are picked
7
+ * one by one, so no token leaves. `user` is the signed-in person's own id, from their session; reading without one fails,
8
+ * since the store would read every person's accounts.
9
+ */
10
+ const listConnections = async ({ findAccounts, connections, events }, user) => {
11
+ if (user === "" || user === void 0 || user === null) throw new Error("Connections are listed for a signed-in person's id");
12
+ const userId = String(user);
13
+ const accounts = await findAccounts(userId);
14
+ const fitting = (connection) => accounts.filter((account) => account.providerId === connection.providerId && holdsScopes(storedScopes(account.scope), connection.declaration));
15
+ const ids = connections.flatMap((connection) => fitting(connection).map((account) => account.id));
16
+ const waiting = new Set(await events.needsSignIn(ids));
17
+ const uses = await events.uses(userId, ids);
18
+ return connections.map((connection) => ({
19
+ key: connection.key,
20
+ label: connection.declaration.label,
21
+ provider: connection.providerId,
22
+ scopes: connection.declaration.scopes ?? [],
23
+ link_params: connection.declaration.link_params ?? {},
24
+ accounts: fitting(connection).map((account) => ({
25
+ id: account.id,
26
+ scopes: storedScopes(account.scope),
27
+ created_at: account.createdAt,
28
+ needs_sign_in: waiting.has(account.id),
29
+ used_by: uses[account.id] ?? []
30
+ }))
31
+ }));
32
+ };
33
+ //#endregion
34
+ export { listConnections };
@@ -1,5 +1,6 @@
1
1
  import { UserColumn, UserTableOptions } from "./user-table.js";
2
2
  import { Mailer } from "../mail/mail-message.js";
3
+ import { Session } from "../types.js";
3
4
  import { SignInPolicy } from "./sign-in-policy.js";
4
5
  import { StaffSignIn } from "./staff-log.js";
5
6
  import { Person } from "./organizations/people-endpoint.js";
@@ -3215,7 +3216,11 @@ export declare const createAuth: (options: CreateAuthOptions, plugins?: AuthPlug
3215
3216
  email: string;
3216
3217
  emailVerified: boolean;
3217
3218
  name: string;
3218
- image?: string | null | undefined;
3219
+ image? /**
3220
+ * Refuses a new password that appears in a known data breach, by Have I Been Pwned's password list: at sign-up, from
3221
+ * an invitation, and when a password is set, changed, reset or given by an admin. Only the first five characters of
3222
+ * the password's SHA-1 hash leave the server. While the list cannot be reached, setting a password fails. Default off.
3223
+ */ : string | null | undefined;
3219
3224
  };
3220
3225
  };
3221
3226
  }>>[];
@@ -3472,6 +3477,10 @@ export declare const createAuth: (options: CreateAuthOptions, plugins?: AuthPlug
3472
3477
  scopes: string[];
3473
3478
  created_at: Date;
3474
3479
  needs_sign_in: boolean;
3480
+ used_by: {
3481
+ id: string;
3482
+ name: string;
3483
+ }[];
3475
3484
  }[];
3476
3485
  }[];
3477
3486
  }>;
@@ -9339,12 +9348,32 @@ export declare const createAuth: (options: CreateAuthOptions, plugins?: AuthPlug
9339
9348
  signInProviders: string[];
9340
9349
  clients: string[];
9341
9350
  };
9342
- /** The workflow connections people connect accounts through, and the listener (`set`) the workflows give for their links and disconnects. */
9351
+ /**
9352
+ * The workflow connections people connect accounts through, the listener (`set`) the workflows give for their links
9353
+ * and disconnects, and `list`, on the server only, of the session's own person's accounts, as `GET /connections` lists them.
9354
+ */
9343
9355
  connections: {
9344
9356
  declared: ResolvedConnection[];
9345
9357
  events: {
9346
9358
  set: (next: ConnectionListener) => undefined;
9347
9359
  };
9360
+ list: (session: Session) => Promise<{
9361
+ key: string;
9362
+ label: string;
9363
+ provider: string;
9364
+ scopes: string[];
9365
+ link_params: Record<string, string>;
9366
+ accounts: {
9367
+ id: string;
9368
+ scopes: string[];
9369
+ created_at: Date;
9370
+ needs_sign_in: boolean;
9371
+ used_by: {
9372
+ id: string;
9373
+ name: string;
9374
+ }[];
9375
+ }[];
9376
+ }[]>;
9348
9377
  };
9349
9378
  /** The OAuth provider for MCP clients, when on: the endpoint's resource (its URL) and path, and the scope its tokens carry. */
9350
9379
  mcp: {
@@ -12,6 +12,7 @@ import { personalMailer } from "../personal-mail/personal-mailer.js";
12
12
  import { mailLinkHooks } from "../personal-mail/mail-link-hooks.js";
13
13
  import { bothAccountHooks, connectionAccountHooks } from "./connections/connection-account-hooks.js";
14
14
  import { connectionEvents } from "./connections/connection-events.js";
15
+ import { listConnections } from "./connections/list-connections.js";
15
16
  import { connectionProvidersPlugin } from "./connections/connection-oauth-provider.js";
16
17
  import { connectionsPlugin } from "./connections/connections-plugin.js";
17
18
  import { resolveConnections } from "./connections/declared-connections.js";
@@ -552,10 +553,18 @@ const createAuth = (options, plugins = {}) => {
552
553
  signInProviders: socialProviders,
553
554
  clients: connectionClients
554
555
  },
555
- /** The workflow connections people connect accounts through, and the listener (`set`) the workflows give for their links and disconnects. */
556
+ /**
557
+ * The workflow connections people connect accounts through, the listener (`set`) the workflows give for their links
558
+ * and disconnects, and `list`, on the server only, of the session's own person's accounts, as `GET /connections` lists them.
559
+ */
556
560
  connections: {
557
561
  declared: resolvedConnections,
558
- events: { set: events.set }
562
+ events: { set: events.set },
563
+ list: async (session) => listConnections({
564
+ findAccounts: async (userId) => (await auth.$context).internalAdapter.findAccounts(userId),
565
+ connections: resolvedConnections,
566
+ events
567
+ }, session.user.id)
559
568
  },
560
569
  /** The OAuth provider for MCP clients, when on: the endpoint's resource (its URL) and path, and the scope its tokens carry. */
561
570
  mcp: mcp && {
@@ -391,6 +391,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], userTables
391
391
  commented,
392
392
  ...engine && { workflows: {
393
393
  ...engine.types,
394
+ listConnections: auth.connections.list,
394
395
  issueWebhook: (session, id, secret) => issueWebhook(engine.issue, session, id, {
395
396
  ...secret !== void 0 && { secret },
396
397
  origin: new URL(auth.mcp.resource).origin
package/dist/package.js CHANGED
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "0.69.0";
2
+ var version = "0.69.2";
3
3
  //#endregion
4
4
  export { version };
@@ -297,7 +297,8 @@ const actionShape = (model, models, createPages) => shapes.object({
297
297
  link: linkShape(model, models, createPages),
298
298
  ai: aiShape(model)
299
299
  }),
300
- inputs: inputsShape(models)
300
+ inputs: inputsShape(models),
301
+ mailbox: shapes.data
301
302
  }, { requires: [
302
303
  "run",
303
304
  "inputs",
@@ -0,0 +1,28 @@
1
+ import { enablerMayUpdate } from "./enabler-authority.js";
2
+ import { sql } from "kysely";
3
+ //#region src/workflows/connections/connection-uses.ts
4
+ /**
5
+ * The workflows that use a person's accounts: those whose enabled version they turned on and binds one of the
6
+ * accounts in its `connections`, kept only while they may still update the workflow, as a step's token needs.
7
+ */
8
+ const connectionUses = ({ deps, schema, roles }) => async (userId, accountIds) => {
9
+ const uses = {};
10
+ if (accountIds.length === 0) return uses;
11
+ const db = deps().db;
12
+ const { rows } = await sql`select w.id, w.name, v.tenant_id, c.value as account
13
+ from ${sql.id(schema, "workflows")} w
14
+ join ${sql.id(schema, "versions")} v on v.workflow_id = w.id and v.version = w.enabled_version
15
+ cross join lateral jsonb_each_text(coalesce(v.definition -> 'connections', '{}'::jsonb)) c
16
+ where v.enabled_by = ${userId} and c.value in (${sql.join(accountIds)})
17
+ order by w.name, w.id`.execute(db);
18
+ for (const row of rows) {
19
+ if (!await enablerMayUpdate(db, deps(), roles, userId, row.tenant_id ?? void 0, row.id)) continue;
20
+ uses[row.account] = [...uses[row.account] ?? [], {
21
+ id: row.id,
22
+ name: row.name
23
+ }];
24
+ }
25
+ return uses;
26
+ };
27
+ //#endregion
28
+ export { connectionUses };
@@ -0,0 +1,15 @@
1
+ import { Session } from "../../types.js";
2
+ import "../../index.js";
3
+ //#region src/workflows/mcp/connection-tools.d.ts
4
+ type Connection = {
5
+ key: string;
6
+ label: string;
7
+ accounts: {
8
+ id: string;
9
+ scopes: string[];
10
+ needs_sign_in: boolean;
11
+ }[];
12
+ };
13
+ /** The session's own person's connections, each with their accounts that fit it, such as `auth.connections.list`. */
14
+ export type ListConnections = (session: Session) => Promise<Connection[]>;
15
+ //#endregion
@@ -0,0 +1,32 @@
1
+ import { defineTool } from "../../assistant/tools.js";
2
+ //#region src/workflows/mcp/connection-tools.ts
3
+ /**
4
+ * The caller's own accounts, by connection, which a definition's `connections` binds by id. Each field is picked one by
5
+ * one, so nothing more than the list reads leaves, and never a token; it takes no argument, so it lists no one else's.
6
+ */
7
+ const listConnectionsTool = (list) => defineTool({
8
+ name: "list_connections",
9
+ description: "Lists the connections the app's workflow plugins declare with your own connected accounts, as JSON ({ connections }), each with its key, label and accounts, each account with its id, scopes and needs_sign_in. A workflow binds an account in its definition's connections, as { \"<key>\": \"<account id>\" }; an account is connected, or connected again when it needs a sign-in, on the account page.",
10
+ parameters: {
11
+ type: "object",
12
+ properties: {}
13
+ },
14
+ annotations: {
15
+ readOnlyHint: true,
16
+ openWorldHint: false
17
+ },
18
+ run: async (_args, context) => {
19
+ const connections = list ? await list(context.session) : [];
20
+ return JSON.stringify({ connections: connections.map(({ key, label, accounts }) => ({
21
+ key,
22
+ label,
23
+ accounts: accounts.map(({ id, scopes, needs_sign_in }) => ({
24
+ id,
25
+ scopes,
26
+ needs_sign_in
27
+ }))
28
+ })) });
29
+ }
30
+ });
31
+ //#endregion
32
+ export { listConnectionsTool };
@@ -4,7 +4,7 @@ import { limits } from "@protobase/workflows";
4
4
  * profile of the States Language with JSONata, and how the workflow tools find, make, change and test-run them.
5
5
  */
6
6
  const workflowInstructions = [
7
- "Workflows: list_workflow_types names the step types and triggers with their arguments and inputs; list_workflows and get_workflow read workflows; validate_workflow checks a definition without saving it; create_workflow and update_workflow save one; run_workflow starts a run with an input, and get_workflow_run reads it with each step's input, output, logs and error. A run goes on in the background: read it again until its status is succeeded or failed, then fix the step that failed with update_workflow and run it again. A trigger starts runs only once enable_workflow turned the workflow on. A webhook trigger whose sender you give a URL, such as app:webhook, gets its URL and secret from new_webhook_url, shown once; one that registers its own URL with its service as the workflow is turned on needs none, and new_webhook_url refuses it.",
7
+ "Workflows: list_workflow_types names the step types and triggers with their arguments and inputs; list_workflows and get_workflow read workflows; validate_workflow checks a definition without saving it; create_workflow and update_workflow save one; run_workflow starts a run with an input, and get_workflow_run reads it with each step's input, output, logs and error. A run goes on in the background: read it again until its status is succeeded or failed, then fix the step that failed with update_workflow and run it again. A trigger starts runs only once enable_workflow turned the workflow on. A webhook trigger whose sender you give a URL, such as app:webhook, gets its URL and secret from new_webhook_url, shown once; one that registers its own URL with its service as the workflow is turned on needs none, and new_webhook_url refuses it. A step or trigger that signs in through a connection uses the account the definition's \"connections\" binds, { \"<connection key>\": \"<account id>\" }: list_connections gives your accounts.",
8
8
  "A definition is one JSON document: { \"trigger\": <trigger>, \"stateMachine\": { \"QueryLanguage\": \"JSONata\", \"StartAt\": \"<state>\", \"States\": { \"<state>\": <state>, ... } } }, the States Language with JSONata expressions, in a profile where every value is read by name. It holds no layout. <trigger> is an object with one key: { \"<trigger id>\": <its input> } for a trigger list_workflow_types names, or { \"manual\": {} } for a workflow that runs only when someone starts it.",
9
9
  "States: Task { Resource: <step type>, Arguments, TimeoutSeconds, Retry, Catch }; Pass { Output, Assign }; Choice { Choices: [{ Condition, Next, Output?, Assign? }], Default }; Map { Items, ItemProcessor: { StartAt, States }, MaxConcurrency }; Wait { Seconds | Timestamp }; Succeed { Output }; Fail { Error, Cause }. Every state may have Comment, Output and Assign. Task, Pass, Map and Wait have exactly one of Next (a state name) and End: true. State names are unique across the document, Maps included, and every state must be reachable from StartAt. InputPath, Parameters, ResultPath and the other JSONPath fields, and Parallel, are not part of the profile.",
10
10
  "Expressions: a string that is whole \"{% ... %}\" is a JSONata expression whose value, of any type, the field takes; any other string is literal text. Build text inside one expression with &: \"{% 'Order ' & $order.id %}\". Expressions sit at any depth in Arguments, Output and Assign, and in Items, Condition, Seconds, Timestamp, Error and Cause.",
@@ -1,5 +1,6 @@
1
1
  import { AssistantTool, ToolContext } from "../../assistant/tools.js";
2
2
  import { IssueWebhook } from "./webhook-tools.js";
3
+ import { ListConnections } from "./connection-tools.js";
3
4
  import "../../index.js";
4
5
  import { ResourceModel } from "@protobase/schema";
5
6
  import { CheckOptions, TypeEntry } from "@protobase/workflows";
@@ -13,7 +14,9 @@ export type WorkflowCatalog = {
13
14
  checkWith: (models: (name: string) => ResourceModel | undefined) => CheckOptions;
14
15
  /** Issues a workflow's webhook URL as the caller, for `new_webhook_url`. */
15
16
  issueWebhook?: IssueWebhook;
17
+ /** The caller's own connected accounts, for `list_connections`; none without it. */
18
+ listConnections?: ListConnections;
16
19
  };
17
- /** The tools that read workflows, their types and runs, and check definitions. */
20
+ /** The tools that read workflows, their types and runs and the caller's connections, and check definitions. */
18
21
  export declare const workflowReadTools: (catalog: WorkflowCatalog) => AssistantTool<ToolContext>[];
19
22
  //#endregion
@@ -1,5 +1,6 @@
1
1
  import { defineTool } from "../../assistant/tools.js";
2
2
  import { refused } from "../../assistant/record-tool-args.js";
3
+ import { listConnectionsTool } from "./connection-tools.js";
3
4
  import { mergePatch } from "./merge-patch.js";
4
5
  import { definitionParameter, editParameter, idOf, idParameter, objectOf, pageSizeOf, textOf } from "./workflow-tool-args.js";
5
6
  import { checkDefinition, typeCatalog } from "@protobase/workflows";
@@ -194,13 +195,14 @@ const getWorkflowRunTool = defineTool({
194
195
  }), refused);
195
196
  }
196
197
  });
197
- /** The tools that read workflows, their types and runs, and check definitions. */
198
+ /** The tools that read workflows, their types and runs and the caller's connections, and check definitions. */
198
199
  const workflowReadTools = (catalog) => [
199
200
  listWorkflowTypesTool(catalog),
200
201
  listWorkflowsTool,
201
202
  getWorkflowTool,
202
203
  validateWorkflowTool(catalog),
203
- getWorkflowRunTool
204
+ getWorkflowRunTool,
205
+ listConnectionsTool(catalog.listConnections)
204
206
  ];
205
207
  //#endregion
206
208
  export { getWorkflowRunTool, getWorkflowTool, listWorkflowsTool, workflowReadTools };
@@ -11,6 +11,7 @@ import { triggerJobs } from "./triggers/trigger-jobs.js";
11
11
  import { workflowTypes } from "./workflow-types.js";
12
12
  import { betterAuthConnectionAccounts } from "./connections/connection-accounts.js";
13
13
  import { connectionWiring } from "./connections/connection-wiring.js";
14
+ import { connectionUses } from "./connections/connection-uses.js";
14
15
  import { connectionTriggers } from "./connections/connection-triggers.js";
15
16
  import { secretRedactor } from "@protobase/workflows";
16
17
  //#region src/workflows/workflow-engine.ts
@@ -60,7 +61,14 @@ const workflowEngine = (options, deps, auth) => {
60
61
  origin: () => auth?.origins[0],
61
62
  systemPath: () => deps().systemPath
62
63
  });
63
- auth?.connections.events.set(connectionTriggers(() => deps().db, schema, triggers));
64
+ auth?.connections.events.set({
65
+ ...connectionTriggers(() => deps().db, schema, triggers),
66
+ uses: connectionUses({
67
+ deps,
68
+ schema,
69
+ roles
70
+ })
71
+ });
64
72
  const webhooks = usable(types.webhooks);
65
73
  const issue = {
66
74
  deps,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/server",
3
- "version": "0.69.0",
3
+ "version": "0.69.2",
4
4
  "description": "The Protobase REST API as a Hono app, with Better Auth sign-in.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -44,11 +44,11 @@
44
44
  "@better-auth/sso": "^1.7.12",
45
45
  "@livekit/protocol": "1.53.1",
46
46
  "@modelcontextprotocol/server": "^2.3.1",
47
- "@protobase/layout": "0.69.0",
48
- "@protobase/query": "0.69.0",
49
- "@protobase/schema": "0.69.0",
50
- "@protobase/workflow-plugins": "0.69.0",
51
- "@protobase/workflows": "0.69.0",
47
+ "@protobase/layout": "0.69.2",
48
+ "@protobase/query": "0.69.2",
49
+ "@protobase/schema": "0.69.2",
50
+ "@protobase/workflow-plugins": "0.69.2",
51
+ "@protobase/workflows": "0.69.2",
52
52
  "@react-email/render": "^2.1.0",
53
53
  "@react-pdf/primitives": "^4.4.0",
54
54
  "@react-pdf/renderer": "^4.9.0",