@protobase/cli 0.71.2 → 0.71.4

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,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "0.71.2";
2
+ var version = "0.71.4";
3
3
  //#endregion
4
4
  export { version };
@@ -1,6 +1,6 @@
1
1
  import { projectFiles, projectFunctions, projectJobs, projectNotifications, projectOptions, projectResources, projectWebhooks, projectWorkflows } from "./project-parts.js";
2
2
  import { userTablesOn } from "./user-tables-config.js";
3
- import { buildAppAuth, buildAuthenticator, checkCallsOptions, checkStatusOptions } from "@protobase/server/internal";
3
+ import { buildAppAuth, buildAuthenticator, checkCallsOptions, checkStatusOptions, workflowPlugins } from "@protobase/server/internal";
4
4
  //#region src/config/build-project.ts
5
5
  /**
6
6
  * The app's own mailer, the default export of its auth/mailer.ts: what sign-in and the jobs (`context.mail`) both send
@@ -34,9 +34,9 @@ const projectAuth = async (config, { db, pool }, workflows) => {
34
34
  ...workflows && { workflowConnections: workflowConnectionsOf(workflows) }
35
35
  });
36
36
  };
37
- /** The connections the workflows' plugins declare, which people connect accounts through at sign-in. */
37
+ /** The connections the workflows' plugins declare, the defaults' among them, which people connect accounts through at sign-in. */
38
38
  const workflowConnectionsOf = (workflows) => ({
39
- declared: (workflows.plugins ?? []).flatMap(({ plugin }) => Object.entries(plugin.connections ?? {}).map(([name, declaration]) => ({
39
+ declared: workflowPlugins(workflows.plugins).flatMap(({ plugin }) => Object.entries(plugin.connections ?? {}).map(([name, declaration]) => ({
40
40
  plugin: plugin.name,
41
41
  name,
42
42
  declaration
@@ -192,7 +192,7 @@ const projectWebhooks = async (config) => {
192
192
  ...ownerRoles && { ownerRoles }
193
193
  };
194
194
  };
195
- const workflowPlugins = async (config, packages) => {
195
+ const workflowPlugins$1 = async (config, packages) => {
196
196
  const project = config.sources.at(-1).files;
197
197
  const fromPackages = await Promise.all(packages.map(async (name) => ({
198
198
  plugin: (await project.package(name)).default,
@@ -209,7 +209,7 @@ const projectWorkflows = async (config) => {
209
209
  if (!config.workflows) return void 0;
210
210
  const { plugins: packages = [], ...options } = config.workflows;
211
211
  const savedByRoles = await lastDefault(config, "workflows/saved-by-roles.ts");
212
- const plugins = await workflowPlugins(config, packages);
212
+ const plugins = await workflowPlugins$1(config, packages);
213
213
  return {
214
214
  ...options,
215
215
  ...savedByRoles && { savedByRoles },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/cli",
3
- "version": "0.71.2",
3
+ "version": "0.71.4",
4
4
  "description": "The protobase command: scaffold, doctor, dev, build and serve a Protobase project.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -43,13 +43,13 @@
43
43
  "dependencies": {
44
44
  "@better-auth/core": "^1.7.12",
45
45
  "@hono/node-server": "^2.1.3",
46
- "@protobase/config": "0.71.2",
47
- "@protobase/layout": "0.71.2",
48
- "@protobase/query": "0.71.2",
49
- "@protobase/schema": "0.71.2",
50
- "@protobase/server": "0.71.2",
51
- "@protobase/ui": "0.71.2",
52
- "@protobase/workflows": "0.71.2",
46
+ "@protobase/config": "0.71.4",
47
+ "@protobase/layout": "0.71.4",
48
+ "@protobase/query": "0.71.4",
49
+ "@protobase/schema": "0.71.4",
50
+ "@protobase/server": "0.71.4",
51
+ "@protobase/ui": "0.71.4",
52
+ "@protobase/workflows": "0.71.4",
53
53
  "@react-email/render": "^2.1.0",
54
54
  "@scalar/hono-api-reference": "^0.12.10",
55
55
  "@sentry/vite-plugin": "^11.10.0",
@@ -25,4 +25,5 @@ Graphs of steps the server runs along their edges, written in a language a model
25
25
  - [Guarded requests for plugins](../reference/workflow-plugins.md#sending-requests) (beta): A plugin's requests reach only public addresses, follow no redirect and read at most 10 MiB.
26
26
  - [Webhook trigger](../reference/workflows.md#appwebhook) (beta): A workflow starts from a signed call to a URL of its own, limited in size and rate.
27
27
  - [Polling triggers](../reference/workflow-plugins.md#polling-and-webhook) (beta): A workflow starts once for each new item a plugin finds on a schedule, or at each time of a cron with `app:schedule`.
28
+ - [Incoming Gmail mail](../reference/workflow-plugins.md#incoming-gmail-mail) (beta): A workflow starts once for each new mail on a label of a connected Gmail mailbox that passes optional sender, recipient and subject filters, read with read-only access within about a minute; not yet checked against a real Google account.
28
29
  - [Connected accounts](../reference/workflows.md#connecting-an-account) (beta): People connect their own accounts at other services through OAuth from the account page, see which workflows use each one, and MCP clients list them to bind. A workflow's steps and triggers sign in with one only once its owner turned the workflow on, with tokens encrypted and refreshed. Disconnecting deletes the token and revokes it at a provider that has a revocation endpoint, trying again for a few hours when the provider does not take it, and says which it did.
@@ -8,7 +8,7 @@ Add trigger and step types to workflows from a file of the app or a package, thr
8
8
  Plugins add step types and record triggers. They also add polling and webhook triggers, which run within the limits named in their section, and connections that sign in to other services as an account a person connected, within the limits named under [Connections](#connections).
9
9
  :::
10
10
 
11
- A **workflow plugin** adds trigger and step types to an app's [workflows](workflows.md). Its types are named `<plugin>:<key>`, such as `acme:create_invoice`: a Task names a step type as its `Resource`, and a definition's `trigger` holds a trigger type with its input. The built-in types, `app:http`, `app:records.*`, `app:mail.send` and `app:record`, are a plugin like any other, written on this interface alone, so a plugin's types run, are checked and are listed exactly as they are.
11
+ A **workflow plugin** adds trigger and step types to an app's [workflows](workflows.md). Its types are named `<plugin>:<key>`, such as `acme:create_invoice`: a Task names a step type as its `Resource`, and a definition's `trigger` holds a trigger type with its input. The built-in types, `app:http`, `app:records.*`, `app:mail.send` and `app:record`, are a plugin like any other, written on this interface alone, so a plugin's types run, are checked and are listed exactly as they are. So is every [default plugin](#default-plugins), such as [incoming Gmail mail](#incoming-gmail-mail).
12
12
 
13
13
  ## Writing a plugin
14
14
 
@@ -46,6 +46,8 @@ export default definePlugin({
46
46
  })
47
47
  ```
48
48
 
49
+ For a whole plugin to read, with a connection, a polling cursor, a refused token and bounded run input, see the Gmail plugin's source, `packages/workflow-plugins/src/gmail/` in the repository, which [the next section](#incoming-gmail-mail) describes.
50
+
49
51
  `definePlugin` checks the plugin's names: its `name` is lowercase letters, digits and `_`, each type's key is lowercase words joined by `_` or `.`, and a type that names a connection must find it among the plugin's `connections`.
50
52
 
51
53
  ### Installing it
@@ -53,7 +55,7 @@ export default definePlugin({
53
55
  - **The app's own:** the default export of `workflows/plugins/<name>.ts`, found by its path as jobs are. An [extension](config.md#extensions) adds its own from its folder.
54
56
  - **From a package:** listed by name in [`workflows.plugins`](config/jobs-and-events.md#workflowsplugins), its default export the plugin. The package is a dependency of the app.
55
57
 
56
- Two plugins of one name stop startup, naming both. A plugin is server code that runs with the app's rights, like the app's own code: load only plugins you trust. The interface's version is `pluginApi`, `1`; releases add optional keys to it and never break it.
58
+ Two plugins of one name stop startup, naming both. The exception is an app's own plugin named like a [default plugin](#default-plugins) other than `app`, which takes that default's place, such as one that pins its own version. A plugin is server code that runs with the app's rights, like the app's own code: load only plugins you trust. The interface's version is `pluginApi`, `1`; releases add optional keys to it and never break it.
57
59
 
58
60
  ## Step types
59
61
 
@@ -91,6 +93,10 @@ A plugin sends through `context.fetch`, never through the global `fetch`, Node's
91
93
 
92
94
  ### Caching results
93
95
 
96
+ :::caution[Beta]
97
+ <gap>A run cannot be replayed with its outside writes not sent and its reads served from the cache because replaying a run is not implemented.</gap>
98
+ :::
99
+
94
100
  A step type can reuse the result of an identical earlier call, from any run of any workflow, so a slow or costly lookup with a small result runs once. Caching is for such steps: a quick read, such as a local file, runs faster than a lookup in the cache, and only fills the app's database. A type without `cache` costs nothing: the engine looks up, keeps and tracks nothing for it.
95
101
 
96
102
  ```ts
@@ -112,11 +118,11 @@ lookup: {
112
118
  | `key` | What a call's key covers. `reads` (default): only the input values `run` read, everything it serialised, listed or handed to `fetch` counting as read. `input`: the whole input. A function of the input: its value, as JSON; one that throws or gives nothing leaves the call uncached. Under `reads`, a value read only after `run` returned is not in the key, so a type that reads its input in a callback later uses `input`. |
113
119
  | `actsAs` | `records`, as the person who saved the version, and `mailbox`, as the person who turned it on: the services a cached call may act through, each adding that person to the key. |
114
120
 
115
- Two calls share a result only when their keys match and they act as the same people: the run's tenant always; the saver and the enabler where `actsAs` lists them; and the account the version binds to the type's connection, with the person who turned it on. The app reads these from the run, never from the step. A call that used `records` or `mailbox` without listing them, or touched the run's files, which are the run's alone, runs as always and is not kept. Only a call that succeeded is kept, at most 256 KiB of it as the run records it, so secrets are blanked out. Each type keeps at most 1,000 results per tenant, and the ones that expire soonest go first. A cache that cannot be read within two seconds or written leaves the step to run without it.
121
+ Two calls share a result only when their keys match and they act as the same people: the run's tenant always; the saver and the enabler where `actsAs` lists them; and the account the version binds to the type's connection, with the person who turned it on. The app reads these from the run, never from the step. A call that used `records` or `mailbox` without listing them, or touched the run's files, which are the run's alone, runs as always and is not kept. Only a call that succeeded with a result is kept, at most 256 KiB of it as the run records it, so secrets are blanked out. Each type keeps at most 1,000 results per tenant, and the ones that expire soonest go first. A cache that cannot be read within two seconds or written leaves the step to run without it.
116
122
 
117
- A workflow forces the cache on with `Cache: true` on its Task, even for a type without `cache` or with a hidden one: such a type keys on its whole input and on every person a step can act as, and keeps a result for an hour. A Task with `Cache: false` never uses it.
123
+ A workflow forces the cache on with `Cache: true` on its Task, even for a type without `cache` or with a hidden one. A type without `cache` then keys on its whole input and on every person a step can act as, and keeps a result for an hour; a type with `cache` keeps its own `ttl`, `key`, `use` and `actsAs`. A Task with `Cache: false` never uses it.
118
124
 
119
- Secrets are never kept. A step always gets a secret's current value, and a key holds the secret's name where the input held its value, so changing a secret and running again calls the step with the new value whenever the old result no longer matches. A hit whose input named a secret that has changed since says so in the step's logs, and in `secrets_changed`, by a fingerprint of the value under the app's secret, `PROTOBASE_SECRET`. Without one, the app cannot tell. A cached step that decides on access, such as whether someone may do something, keeps its answer until its result expires. The results are kept in the workflows schema's `step_cache`. <gap>A run cannot be replayed with its outside writes not sent and its reads served from the cache because replaying a run is not implemented.</gap>
125
+ Secrets are never kept. A step always gets a secret's current value, and a key holds the secret's name where the input held its value, so changing a secret and running again calls the step with the new value whenever the old result no longer matches. A hit whose input named a secret that has changed since says so in the step's logs, and in `secrets_changed`, by a fingerprint of the value under the app's secret, `PROTOBASE_SECRET`. Without one, the app cannot tell. A cached step that decides on access, such as whether someone may do something, keeps its answer until its result expires. The results are kept in the workflows schema's `step_cache`.
120
126
 
121
127
  ## Trigger types
122
128
 
@@ -231,6 +237,57 @@ The token is blanked out of the step's logs, error and output, and out of the tr
231
237
 
232
238
  An account connected through a provider of a connection's own never signs anyone in to the app, and never signs anyone up. <gap>A connection cannot be shared between people of an organization because shared connections are not implemented; each account is its owner's alone.</gap>
233
239
 
240
+ ## Default plugins
241
+
242
+ Every app has these plugins besides `app`. A type that signs in is offered once the app can make its connection, as [Connections](#connections) says.
243
+
244
+ ### Incoming Gmail mail
245
+
246
+ :::caution[Beta]
247
+ The Gmail plugin is tested against a stand-in of Gmail's API written to Google's published reference, but has not yet been checked against a real Google account. Mail starts its run within about a minute of arriving, not at once.
248
+ :::
249
+
250
+ `gmail:new_mail` starts one run for each new mail on a label of a connected Gmail mailbox. It reads the mailbox with read-only access, so a workflow it starts cannot send, change or delete mail through it.
251
+
252
+ To use it, turn on Google sign-in (`auth.socialProviders.google`) with `auth.encryptOAuthTokens`, and add the scope `https://www.googleapis.com/auth/gmail.readonly` to the app's Google consent screen (Data Access), as [Send mail from Gmail](../guides/send-from-gmail.md) does for its own scope. `gmail.readonly` is a restricted scope: until Google has verified the app, the consent screen warns that the app is unverified, and at most 100 people can connect. The person whose mailbox it is connects their Google account under **Connections** on the account page, binds that account in the workflow and turns the workflow on:
253
+
254
+ ```json
255
+ { "trigger": { "gmail:new_mail": { "label": "INBOX", "from": "@supplier.example", "subject": "invoice" } }, "connections": { "gmail:google": "<account id>" }, "stateMachine": { ... } }
256
+ ```
257
+
258
+ | Input | What it does |
259
+ | --- | --- |
260
+ | `label` | The label to watch, by name or id. Default `INBOX`. A label the mailbox does not have fails the trigger, naming it. |
261
+ | `from` | Only mail whose sender's name or address contains this. |
262
+ | `to` | Only mail with a To or Cc recipient whose name or address contains this. |
263
+ | `subject` | Only mail whose subject contains this. |
264
+
265
+ Each filter ignores case, and a mail must pass every filter that is set. A mail without the value a filter reads, such as one with no sender, does not pass.
266
+
267
+ A run's input is the mail:
268
+
269
+ ```json
270
+ {
271
+ "message_id": "18c2f0a9d3e4b5a6",
272
+ "thread_id": "18c2f0a9d3e4b5a6",
273
+ "from": { "name": "Ada", "address": "ada@example.com" },
274
+ "to": [{ "name": "Grace", "address": "grace@example.com" }],
275
+ "cc": [],
276
+ "subject": "Invoice 7",
277
+ "date": "2026-11-05T10:00:00.000Z",
278
+ "text": "Attached.",
279
+ "html": "<p>Attached.</p>",
280
+ "attachments": [{ "filename": "invoice-7.pdf", "content_type": "application/pdf", "size": 48213 }],
281
+ "truncated": false
282
+ }
283
+ ```
284
+
285
+ - **The values** are as the mail states them, and `null` where it states none. `html` is the sender's own markup: treat it as untrusted wherever a workflow passes it on. Attachments come as their names, types and sizes, not their content.
286
+ - **Bounds:** each value has a fixed most length, such as 1,000 characters for the subject and 128 Ki characters for `text` and for `html`, and at most 50 recipients of each kind and 50 attachments. When the mail is still too large, `html` and then `text` are left out. A mail over about 7.5 MiB, whose answer from Gmail, in base64, is then over 10 MiB, or one too deeply nested to read, gives its headers alone, with `text`, `html` and `attachments` as `null`. A NUL character, or half of a character's surrogate pair, is left out, since the database cannot store it. `truncated` is `true` whenever something was cut short or left out.
287
+ - **When it starts:** from the moment the workflow is turned on. Mail already in the mailbox starts no run.
288
+ - **Polling:** every minute, the trigger reads the mailbox's history after where it stood, at most 90 mails a poll and the rest on the next poll, so each mail starts one run and none is skipped. Every mail it reads counts toward the 90, whether or not it passes the filters. Two triggers on one mailbox share Google's quota for that mailbox. When Google no longer keeps the history after the trigger's place, which can happen after a long time off, the trigger lists the label's mail since its last full read instead, 90 a poll, and then goes on from the history. <gap>A new Gmail mail cannot start its run at once because push notices from the mailbox are not implemented; the trigger polls every minute.</gap>
289
+ - **Sign-in:** the token is refreshed when it expires. When Google refuses the refresh, or refuses the token twice in a row, the trigger waits for the account's owner to connect it again, as [Polling and webhook](#polling-and-webhook) describes.
290
+
234
291
  ## The type list
235
292
 
236
293
  [`list_workflow_types`](mcp.md#workflows) over MCP lists every step and trigger type the app has, built-in and plugin ones alike, as their plugins declare them: id, label, description, input and output schemas, errors, connection and a step type's cache advice (`choice`, `default` and `disclaimer`), with `available: false` and a reason for a type the app cannot run. A save, and `validate_workflow`, check a definition against the same list.