@fabrika/app 0.6.0 → 0.8.0
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/package.json +10 -9
- package/skill/SKILL.md +29 -21
- package/skill/references/app-setup.md +58 -36
- package/skill/references/dev.md +11 -9
- package/skill/references/endpoint.md +154 -25
- package/skill/references/errors.md +26 -23
- package/skill/references/files.md +187 -137
- package/skill/references/jobs.md +82 -35
- package/skill/references/public-intake.md +450 -255
- package/skill/references/spa.md +6 -5
- package/skill/references/start.md +2 -1
- package/skill/references/table-and-migration.md +24 -2
- package/skill/references/tests.md +14 -8
- package/skill/references/webhooks-links-connectors.md +65 -12
- package/src/bun.ts +3 -2
- package/src/client/contract.ts +221 -79
- package/src/client/index.ts +3 -1
- package/src/client/transport.ts +7 -2
- package/src/cloudflare.ts +2 -0
- package/src/contract/artifact-schema.ts +168 -14
- package/src/contract/build.ts +1282 -619
- package/src/contract/compile.ts +44 -10
- package/src/contract/connect.ts +79 -5
- package/src/contract/context.ts +132 -23
- package/src/contract/declaration.ts +78 -63
- package/src/contract/defaults.ts +142 -60
- package/src/contract/define.ts +92 -121
- package/src/contract/file.ts +78 -0
- package/src/contract/group.ts +244 -37
- package/src/contract/index.ts +18 -0
- package/src/contract/kit.ts +206 -126
- package/src/contract/platform.ts +8 -9
- package/src/contract/redirect.ts +49 -0
- package/src/contract/schema-from-shape.ts +7 -0
- package/src/contract/schema-shape.ts +10 -0
- package/src/contract/snapshot-source.ts +5 -0
- package/src/contract/token.ts +89 -0
- package/src/contract/webhook-binding.ts +26 -0
- package/src/contract/webhook-scheme.ts +32 -0
- package/src/files/handle.ts +22 -0
- package/src/files/kinds.ts +454 -0
- package/src/files/scan.ts +15 -9
- package/src/files/streams.ts +111 -0
- package/src/honeypot.ts +7 -0
- package/src/implement/anonymous.ts +18 -42
- package/src/implement/boot.ts +210 -157
- package/src/implement/bounded.ts +23 -0
- package/src/implement/browser-config.ts +6 -5
- package/src/implement/capability.ts +78 -2
- package/src/implement/challenge.ts +203 -47
- package/src/implement/{form-admission.ts → client-address.ts} +3 -38
- package/src/implement/dispatch.ts +831 -409
- package/src/implement/egress.ts +84 -0
- package/src/implement/faults.ts +11 -1
- package/src/implement/index.ts +3 -2
- package/src/implement/invocation.ts +135 -35
- package/src/implement/params.ts +88 -0
- package/src/implement/principal.ts +7 -0
- package/src/implement/public.ts +167 -0
- package/src/implement/tokens.ts +219 -0
- package/src/implement/types.ts +24 -3
- package/src/implement/urls.ts +78 -0
- package/src/implement/verifiers.ts +299 -0
- package/src/implement/webhook.ts +136 -55
- package/src/implement/workload.ts +16 -14
- package/src/index.ts +3 -0
- package/src/jobs/mail.ts +98 -10
- package/src/jobs/runner.ts +103 -71
- package/src/jobs/scrub.ts +4 -1
- package/src/jobs/types.ts +19 -2
- package/src/react/hooks.ts +2 -2
- package/src/react/provider.tsx +2 -2
- package/src/testing/challenge.ts +35 -1
- package/src/testing/dev.ts +2 -2
- package/src/testing/endpoint-access.ts +168 -128
- package/src/testing/kit.ts +90 -25
- package/src/testing/policy-isolation.ts +55 -29
- package/src/testing/proxy.ts +6 -2
- package/src/testing/sample-file.ts +30 -0
- package/src/client/files.ts +0 -135
- package/src/contract/file-schemas.ts +0 -25
- package/src/files/collections.ts +0 -848
package/skill/references/spa.md
CHANGED
|
@@ -12,6 +12,7 @@ dependencies, `tsconfig.json` (`"jsx": "react-jsx"`) and `build-web.ts`.
|
|
|
12
12
|
| `@fabrika/app/react` | `FabrikaProvider`, `useQuery`, `useInfiniteQuery`, `useMutation`, `useAction`, `useDownload`, `useInvalidate`, `skip`, `useCaller`, `Can`, `FabrikaError` |
|
|
13
13
|
| `@fabrika/app/react/testing` | `installDom`, `type`, `controlByLabel`, `waitForText`: a screen test's DOM and typing |
|
|
14
14
|
| `@fabrika/ui` | the components below, `createRouter`, and `@fabrika/ui/styles.css` |
|
|
15
|
+
| `@fabrika/ui/challenge` | `Challenge`: the ALTCHA widget of a public procedure's challenge, at `challengeUrl(api.<procedure>)`; needs the `altcha` package (`public-intake.md`) |
|
|
15
16
|
|
|
16
17
|
## The provider and the routes
|
|
17
18
|
|
|
@@ -356,10 +357,10 @@ export function EditTask(
|
|
|
356
357
|
- `Form` shows its `error`: a summary on top, and each `fields[name]` under the `Field` of that `name`. `Field` gives
|
|
357
358
|
its control the `name`, an `id`, its label and `aria-describedby`; the control needs nothing else. `Form` hands
|
|
358
359
|
`onSubmit` the `FormData`: read the fields by name, and leave the checking to the procedure's schema.
|
|
359
|
-
- A call that is no procedure
|
|
360
|
-
runs through `useAction(async (input) => …)`, with the same state and error union; it refetches only what
|
|
360
|
+
- A call that is no procedure of a client runs through `useAction(async (input) => …)`, with the same state and error union; it refetches only what
|
|
361
361
|
`invalidates` names.
|
|
362
|
-
- An attachment procedure (`output: attachment(…)`)
|
|
362
|
+
- An attachment procedure (`output: attachment(…)`) or a file procedure (`output: file('<kind>')`) downloads through
|
|
363
|
+
`useDownload(api.reports.exportProject)`:
|
|
363
364
|
`mutate(input)` saves the file and answers it, with the same state and error union. It runs on the provider's client,
|
|
364
365
|
so a screen test downloads over the kit; never call `download(api.…)` of the module's client from a screen.
|
|
365
366
|
|
|
@@ -737,9 +738,9 @@ if (postgres !== null) {
|
|
|
737
738
|
|
|
738
739
|
- `web/__tests__/dom.ts` comes first: imports run in order, and React reads `window` when it loads. `installDom` keeps
|
|
739
740
|
Bun's `fetch`, `AbortController`, `URL`, `Blob`, `File`, `FormData` and its `ReadableStream`, `WritableStream` and
|
|
740
|
-
`TransformStream`, because the clients, the kit, a
|
|
741
|
+
`TransformStream`, because the clients, the kit, a challenge, a file field and a download run on them, and
|
|
741
742
|
turns navigation off, so a download's link click leaves the page for the next test. A DOM registration of your own
|
|
742
|
-
keeps the same classes of Bun, or
|
|
743
|
+
keeps the same classes of Bun, or an input with a file fails in the kit. Run the tests with `bun test --isolate`, which
|
|
743
744
|
gives each file its own globals.
|
|
744
745
|
- `await type('Title', 'report')` types into the control whose label (or `aria-label`) is `Title` as React reads a
|
|
745
746
|
person typing; a select takes the option's value. `controlByLabel(label)` answers the control, and
|
|
@@ -73,7 +73,8 @@ export default serve(contract, { database: null })
|
|
|
73
73
|
- `defineApp(vocabulary)` declares no tables, so `serve` takes `database: null`, and only then. A handler has no
|
|
74
74
|
`ctx.db`, `ctx.sql` or `unscoped` (a type error that names `defineApp(vocabulary, tables)`); it answers from the
|
|
75
75
|
caller's grants (`ctx.within.values`, `ctx.principal`), the member directory, or a connector.
|
|
76
|
-
-
|
|
76
|
+
- Jobs, queue tables, file columns and public procedures that record a row all need tables, so an application gets
|
|
77
|
+
them with its first table.
|
|
77
78
|
|
|
78
79
|
```ts fabrika.config.ts
|
|
79
80
|
import { compileContract, schemaFromContract } from '@fabrika/app/contract'
|
|
@@ -149,7 +149,7 @@ INSERT INTO priorities (code, label, rank) VALUES ('low', 'Low', 1), ('normal',
|
|
|
149
149
|
- Codegen types: `date` is a `YYYY-MM-DD` string; `timestamptz` reads a `Date` and takes a `Date` or a strict ISO 8601 string with `Z` or an offset;
|
|
150
150
|
`int8` and `numeric` are strings; an enum is its literal union. JSON, `bytea`, arrays and domains are refused.
|
|
151
151
|
- Plain SQL of the application's own schema only. Refused: another schema (`public.`, `app_…`), any `fabrika_*`
|
|
152
|
-
object (
|
|
152
|
+
object (a file column references `fabrika_files (id)`, `references/files.md`), `GRANT`, roles, `CREATE EXTENSION`, `COPY`,
|
|
153
153
|
`SET`, transaction control, `SECURITY DEFINER`, a domain, `serial` and sequences (use `GENERATED ALWAYS AS IDENTITY`),
|
|
154
154
|
a function overload, and calls outside the reviewed built-ins.
|
|
155
155
|
- Never edit, rename or delete a committed migration; add a new one. Change a schema by expand, switch, contract, and
|
|
@@ -223,6 +223,13 @@ export const tables = defineTables(vocabulary, columns)({
|
|
|
223
223
|
insert: ['row_count'],
|
|
224
224
|
createdBy: 'created_by',
|
|
225
225
|
},
|
|
226
|
+
task_attachments: {
|
|
227
|
+
scope: { child: 'tasks' },
|
|
228
|
+
actions: { read: 'tasks.read', insert: 'tasks.write', delete: 'tasks.write' },
|
|
229
|
+
insert: ['file'],
|
|
230
|
+
files: { file: 'attachments' },
|
|
231
|
+
createdBy: 'created_by',
|
|
232
|
+
},
|
|
226
233
|
priorities: {
|
|
227
234
|
scope: { reference: 'the priority codes; a migration changes them' },
|
|
228
235
|
actions: { read: 'tasks.read' },
|
|
@@ -244,7 +251,9 @@ export const tables = defineTables(vocabulary, columns)({
|
|
|
244
251
|
- The key (`id`), the container column (`<container>_id`), a child's column (`<parent dimension>_id`) and the chain
|
|
245
252
|
are derived. State `in: { column }` or `column` only off that convention.
|
|
246
253
|
- `actions` maps `read`, `insert`, `update` and `delete` to vocabulary actions. `insert` and `update` list the columns
|
|
247
|
-
that those actions write; nothing else is writable.
|
|
254
|
+
that those actions write; nothing else is writable. Each operation names one action or a list, any of which allows
|
|
255
|
+
it: a table that staff and a public procedure both read names both (`read: ['crm.read', 'catalog.read']`). Never let
|
|
256
|
+
a role hold a public procedure's action to share a table: list the actions.
|
|
248
257
|
- A dimension or child table may also list `appRead: ['<action>', …]`, actions besides `read` with which an `app: true`
|
|
249
258
|
endpoint lists every row, read-only (an export, a bulk send), with the columns of that action, and
|
|
250
259
|
`appCount: ['<action>', …]`, actions with which it only counts rows (`count({ where })`) and reads none, no aggregate
|
|
@@ -252,6 +261,19 @@ export const tables = defineTables(vocabulary, columns)({
|
|
|
252
261
|
- `columns: { <column>: { read: '<action>' } }` hides a column from every capability of another action, in the data
|
|
253
262
|
and in the types. `columns: { <column>: { write: '<action>' } }` lets only that action update the column (and that
|
|
254
263
|
action updates nothing else); the column is then not in `update`.
|
|
264
|
+
- `files: { <column>: '<kind>' }` makes a column a file column of a kind the contract declares (`c.files`):
|
|
265
|
+
`task_attachments.file` holds the files of a task (`references/files.md`). Its migration writes
|
|
266
|
+
`<column> UUID REFERENCES fabrika_files (id)` and an index the column leads; the checker's `file-column-migration`
|
|
267
|
+
refuses a file column without either.
|
|
268
|
+
- `queue: { attempts: '<column>', due: '<column>', maxAttempts? }` makes the table a queue a job claims
|
|
269
|
+
(`references/jobs.md`). It needs `version` and an `update` action; `<due>` is a timestamp with a default of `now()`
|
|
270
|
+
that allows `null`, `<attempts>` an integer with a default of 0. The data layer writes both; an insert may set
|
|
271
|
+
`<due>`, never `<attempts>`.
|
|
272
|
+
- `insert(values, { onConflict: { columns, do: 'nothing' | 'update' } })` writes idempotently (a webhook delivery, an
|
|
273
|
+
unsubscribe): `columns` must be a unique index the migrations create (`UNIQUE`, a primary key, or a
|
|
274
|
+
`CREATE UNIQUE INDEX` without `WHERE`), which the checker's `on-conflict-index` verifies. `'nothing'` needs the
|
|
275
|
+
insert action and answers `{ inserted: false }` on a conflict; `'update'` also needs the update action and writes
|
|
276
|
+
only the table's update columns.
|
|
255
277
|
- A column has one write action. A state that two actions change is two columns: in `references/public-intake.md` the
|
|
256
278
|
link holder's `candidates.verify` sets `verified_at`, HR's `candidates.review` sets `status`, and `logic.ts` derives
|
|
257
279
|
the stage HR sees from both (`stageOf`).
|
|
@@ -9,7 +9,10 @@ longer writes. Commit them; never edit, format or import them (`generated-test-h
|
|
|
9
9
|
- `src/__tests__/endpoint-access.test.ts`: every `on` and `in` endpoint refuses another scope's object (404 or 403);
|
|
10
10
|
every `on` and `over` endpoint, and every `in` endpoint that declares `sql`, answers nothing of another scope; a
|
|
11
11
|
caller with only an endpoint's write action, plus what IAM requires of it (below), reaches its own object (200). An
|
|
12
|
-
`app` or `creates` endpoint refuses a holder of its action in one scope only (403) and answers a global holder.
|
|
12
|
+
`app` or `creates` endpoint refuses a holder of its action in one scope only (403) and answers a global holder. An
|
|
13
|
+
endpoint of several `capabilities` adds a denied case per capability. A public procedure answers anyone without a
|
|
14
|
+
credential, or, behind a challenge, refuses a caller without a solved one. A file input is sent as a generated file
|
|
15
|
+
of its kind; a `GET` procedure's input goes in its URL.
|
|
13
16
|
- Their kit stands in for IAM, as `testKit` does (below): it creates a scope, assigns or invites a member and issues a
|
|
14
17
|
link under IAM's rules for the caller's grants. A repeated invitation answers the open one, `resend` restarts its
|
|
15
18
|
7 days, and an organization's address is refused for an external role (`internal_mailbox`). So the caller of an endpoint with the dimension's `members.manage`
|
|
@@ -34,8 +37,8 @@ export const members = { 'api.tasks.assign': ['assignee'] }
|
|
|
34
37
|
```
|
|
35
38
|
|
|
36
39
|
- `export const fixtures = { '<endpoint id>': { … } }` is an input for an endpoint whose schema the scaffold cannot
|
|
37
|
-
sample, such as a `regex`
|
|
38
|
-
`{ '
|
|
40
|
+
sample, such as a `regex` (a webhook's account):
|
|
41
|
+
`{ 'api.integrations.connectGithub': { orgId: 'acme', account: 'installation:1' } }`. The checker names each missing one,
|
|
39
42
|
and one of an endpoint that does not exist (`endpoint-fixture`). The access form's `from` field is
|
|
40
43
|
replaced by the seeded scope.
|
|
41
44
|
- `members['<endpoint id>']` lists the input fields that name a picked person the handler checks with
|
|
@@ -284,7 +287,7 @@ if (postgres !== null) {
|
|
|
284
287
|
- `kit.runJob('<job>')` runs a cron job once and answers `{ status: 'succeeded', attempts }`. A job's `ctx.links`
|
|
285
288
|
reaches the kit's stand-in IAM under the job's own key, so a job that declares `issues` issues and revokes links in a
|
|
286
289
|
plain `testKit` and under `fabrika app dev`, as IAM allows it: for its declared mount only.
|
|
287
|
-
- A
|
|
290
|
+
- A mutation that kicks (`kicks`, `ctx.kick`) starts its job in the background of the call. `kit.drain()` waits for
|
|
288
291
|
those runs, and `kit.runJob` first waits for them; every generated access case drains before it answers. The kicked
|
|
289
292
|
run may start at once, so arm a fault before the call, never between the call and the run.
|
|
290
293
|
- `kit.mail` is the stand-in for every job's mail transport (`MemoryEmailSender` of `@fabrika/app/testing`), so a test
|
|
@@ -297,10 +300,13 @@ if (postgres !== null) {
|
|
|
297
300
|
without it the application does not boot. With `scan: true`, an upload stays `quarantined` until
|
|
298
301
|
`kit.runJob(FILE_SCAN_JOB)` (`@fabrika/app/implement`) runs with a `scanner` passed to `testKit`: a `FileScanner`
|
|
299
302
|
stand-in, or the real ClamAV one, `clamscanScanner()` of `@fabrika/app/bun`.
|
|
300
|
-
- A
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
303
|
+
- A challenge passes once: a second submission with the same token answers 400 `challenge_failed`. For an ALTCHA
|
|
304
|
+
challenge, `await kit.solveChallenge(api.<procedure>)` fetches one through the kit's proxy and solves it, so send a
|
|
305
|
+
fresh one with each submission; the kit issues the cheapest challenge and verifies it as production does. For a
|
|
306
|
+
Turnstile challenge send a fresh token with each submission (`crypto.randomUUID()`), also when the test's
|
|
307
|
+
`challengeFetch` answers success. `passingChallenge` (`@fabrika/app/testing`) is a `challengeFetch` that passes every
|
|
308
|
+
token; a stand-in of your own receives the check (`hostname`, `action`) a passing answer must confirm as its second
|
|
309
|
+
argument.
|
|
304
310
|
- `testKit` takes no `deps`, and a test may not start a server (`Bun.serve` is `forbidden-global`). Stand in for an
|
|
305
311
|
outbound dependency through its variables: `env` points it at a test URL, and `spyOn(globalThis, 'fetch')` answers
|
|
306
312
|
that URL only and passes every other request on to the real `fetch`.
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
Each of these lets someone in who is not a signed-in member of the scope. Ask the human before you add one, and say
|
|
4
4
|
what it lets that caller do.
|
|
5
5
|
|
|
6
|
-
## Webhooks
|
|
6
|
+
## Webhooks
|
|
7
7
|
|
|
8
8
|
```ts src/github/endpoints.ts
|
|
9
|
+
import { WebhookSecret } from '@fabrika/app/contract'
|
|
9
10
|
import { z } from 'zod/v4'
|
|
10
11
|
import { app } from '../app-kit'
|
|
11
12
|
|
|
@@ -27,19 +28,71 @@ export const github = app.webhook('/hooks/github', {
|
|
|
27
28
|
name: input.repository.full_name,
|
|
28
29
|
})
|
|
29
30
|
})
|
|
31
|
+
|
|
32
|
+
const Connect = z.strictObject({
|
|
33
|
+
orgId: z.string(),
|
|
34
|
+
account: z.string().regex(/^(installation|repository):\d+$/),
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
/** Binds an org to a GitHub account: answers the secret and the URL once. */
|
|
38
|
+
export const integrations = app.group({ callers: ['human'] }, (r) => ({
|
|
39
|
+
connectGithub: r.mutation({
|
|
40
|
+
input: Connect,
|
|
41
|
+
output: WebhookSecret,
|
|
42
|
+
access: { action: 'members.manage', on: 'org', from: 'orgId' },
|
|
43
|
+
binds: 'github',
|
|
44
|
+
})
|
|
45
|
+
.handle(({ input, ctx }) =>
|
|
46
|
+
ctx.webhooks('github').bind(ctx.target, input.account)
|
|
47
|
+
),
|
|
48
|
+
}))
|
|
30
49
|
```
|
|
31
50
|
|
|
32
|
-
- Mount it by name in the contract: `github,` beside `api`. It answers
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
51
|
+
- Mount it by name in the contract: `github,` beside `api`, and the group in `api`. It answers
|
|
52
|
+
`POST /hooks/github/:bindingId`.
|
|
53
|
+
- A binding is made by a procedure that declares `binds: '<webhook>'` and calls `ctx.webhooks('<webhook>').bind(ref,
|
|
54
|
+
account)` with a capability on the bound value (`ctx.target`, an org here), in its transaction; `unbind(ref, id)`
|
|
55
|
+
removes one. A GitHub account is `installation:<id>` or `repository:<id>`. The answer carries the secret and the URL
|
|
56
|
+
once. The generated tests need a fixture for its input (`references/tests.md`).
|
|
36
57
|
- The framework checks the signature, the account and the input (undeclared fields are dropped).
|
|
37
58
|
`ctx.refs['<action>']` is a `Ref` of the bound org for each declared action.
|
|
38
|
-
- It
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
data work on `ctx.delivery.key`; never rely on exactly one run.
|
|
59
|
+
- It claims the delivery key (a digest of the binding and the body) under a lease before the handler runs, marks it
|
|
60
|
+
complete in the handler's transaction, and answers a body it already ran in the last 30 days without running it
|
|
61
|
+
again. A handler that throws commits nothing, and a process that dies before the commit leaves the lease to expire,
|
|
62
|
+
so GitHub's redelivery runs it again from nothing. So make every effect idempotent, and key an external effect after
|
|
63
|
+
the data work on `ctx.delivery.key`; never rely on exactly one run.
|
|
64
|
+
- An idempotent write needs no `sql`: `insert(values, { onConflict: { columns: ['<unique column>'], do: 'nothing' } })`
|
|
65
|
+
(or `do: 'update'`) on a unique index (`references/table-and-migration.md`).
|
|
66
|
+
- **The verifier.** `scheme` is a preset or a shape the framework computes against the binding's secret; never verify
|
|
67
|
+
a signature yourself:
|
|
68
|
+
|
|
69
|
+
| `scheme` | For |
|
|
70
|
+
| --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
|
|
71
|
+
| `'github-sha256'` | GitHub; it also checks the account |
|
|
72
|
+
| `'standard-webhooks'` | senders of the Standard Webhooks spec |
|
|
73
|
+
| `'stripe'` | Stripe |
|
|
74
|
+
| `'slack'` | Slack events, slash commands and interactivity (JSON or a form) |
|
|
75
|
+
| `'twilio'` | Twilio callbacks (a form, or JSON); signs the public URL |
|
|
76
|
+
| `{ hmac: { header, algorithm, encoding, prefix?, timestamp?, signed: 'body' \| 'timestamp.body', id? } }` | most other HMAC senders (Shopify, …) |
|
|
77
|
+
| `{ basic: true }` | a sender that sends the secret as a Basic password (an inbound mail API) |
|
|
78
|
+
| `{ bearer: true }` | a sender that sends the secret as `Authorization: Bearer <secret>` |
|
|
79
|
+
|
|
80
|
+
A signed timestamp must be within five minutes. Slack's URL check (`url_verification`) is answered for you. A form's
|
|
81
|
+
fields are the input, as strings or the scalars the schema reads.
|
|
82
|
+
- **A secret the provider issues.** Stripe, Slack and Twilio show their signing secret only after you register the
|
|
83
|
+
URL. Bind first, show the URL, then take the provider's secret in a second mutation with the same `binds` and call
|
|
84
|
+
`ctx.webhooks('<webhook>').setSecret(capability, id, secret)`; until then deliveries are refused (401).
|
|
85
|
+
`setSecret` answers `false` when the value has no binding of that id. A `basic` or `bearer` secret never contains
|
|
86
|
+
`px_`: the proxy removes a platform key from every request.
|
|
87
|
+
- **`boundTo: 'app'`** gives one binding for the whole application, for a provider with one endpoint per account
|
|
88
|
+
(Stripe). Its handler holds its `actions` application-wide, as a job does: `ctx.db.app('<action>')`. The procedure
|
|
89
|
+
that binds it takes an `app` capability: `access: { action, app: true }`, `binds`, and
|
|
90
|
+
`ctx.webhooks('<webhook>').bind(ctx.app, account)`.
|
|
91
|
+
- `limits: { maxBytes?, timeoutMs? }`: the body is 1 MiB by default and at most 48 MiB; the handler runs 10 s by
|
|
92
|
+
default, at most 60 s. A webhook may declare `kicks`, `signs`, `egress`, `sql` and `unscoped`, and `ctx.files.put`
|
|
93
|
+
stores a file a provider sends (an inbound e-mail's attachment) for a file column: raise `limits.maxBytes` to the
|
|
94
|
+
mail you accept.
|
|
95
|
+
- Not covered: a provider that validates its URL with a `GET`, or that needs a custom answer body.
|
|
43
96
|
|
|
44
97
|
## Share links
|
|
45
98
|
|
|
@@ -97,8 +150,8 @@ export const shared = app.link('/s', {
|
|
|
97
150
|
that should work once records its use in the row: a `verify` sets `verified_at` once under `getForUpdate`, and a
|
|
98
151
|
second call answers the same. IAM counts no uses.
|
|
99
152
|
- A cron job may issue links too (`issues` on `app.cron`, `references/jobs.md`): a visitor without an account gets a
|
|
100
|
-
link by e-mail from a job that
|
|
101
|
-
the files a link holder uploads
|
|
153
|
+
link by e-mail from a job that claims a public procedure's rows. `references/public-intake.md` is that recipe end to
|
|
154
|
+
end, with the files a link holder uploads through the link's own procedures (`references/files.md`).
|
|
102
155
|
|
|
103
156
|
## Connectors: another application's data
|
|
104
157
|
|
package/src/bun.ts
CHANGED
|
@@ -251,7 +251,7 @@ const STORAGE_VARIABLES = {
|
|
|
251
251
|
} as const
|
|
252
252
|
|
|
253
253
|
/**
|
|
254
|
-
* The
|
|
254
|
+
* The stored files' object storage (Zerops object storage, path-style), when the platform wrote its variables;
|
|
255
255
|
* a partial set stops the boot. The credentials are never quoted in a message.
|
|
256
256
|
*/
|
|
257
257
|
export function openFileStorage(framework: Readonly<Record<string, string>>): FileObjectStore | undefined {
|
|
@@ -394,6 +394,7 @@ async function startContractServer(
|
|
|
394
394
|
workloadTokens,
|
|
395
395
|
requireProxySecret: true,
|
|
396
396
|
localEmailProviders: LOCAL_EMAIL_PROVIDERS,
|
|
397
|
+
connections: databaseConnections(environment.framework[DATABASE_CONNECTIONS]) ?? DEFAULT_DATABASE_CONNECTIONS,
|
|
397
398
|
...(operationsFetch === undefined ? {} : { operationsFetch }),
|
|
398
399
|
})
|
|
399
400
|
// The work a request leaves running (a failure's report, a kicked run) is drained, within a bound, before the database closes.
|
|
@@ -464,7 +465,7 @@ export async function runContractJob(
|
|
|
464
465
|
workloadTokens,
|
|
465
466
|
requireProxySecret: true,
|
|
466
467
|
localEmailProviders: LOCAL_EMAIL_PROVIDERS,
|
|
467
|
-
// ClamAV on PATH, as the provider's build installs it for a `scan: true`
|
|
468
|
+
// ClamAV on PATH, as the provider's build installs it for a `scan: true` file kind; missing, the scan fails closed.
|
|
468
469
|
scanner: clamscanScanner(),
|
|
469
470
|
})
|
|
470
471
|
.jobs.run(name, options)
|