@fabrika/app 0.6.0 → 0.7.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.
Files changed (80) hide show
  1. package/package.json +9 -9
  2. package/skill/SKILL.md +29 -21
  3. package/skill/references/app-setup.md +54 -36
  4. package/skill/references/dev.md +5 -5
  5. package/skill/references/endpoint.md +152 -25
  6. package/skill/references/errors.md +26 -23
  7. package/skill/references/files.md +187 -137
  8. package/skill/references/jobs.md +82 -35
  9. package/skill/references/public-intake.md +387 -236
  10. package/skill/references/spa.md +5 -5
  11. package/skill/references/start.md +2 -1
  12. package/skill/references/table-and-migration.md +24 -2
  13. package/skill/references/tests.md +8 -5
  14. package/skill/references/webhooks-links-connectors.md +65 -12
  15. package/src/bun.ts +3 -2
  16. package/src/client/contract.ts +184 -74
  17. package/src/client/index.ts +2 -1
  18. package/src/client/transport.ts +7 -2
  19. package/src/cloudflare.ts +2 -0
  20. package/src/contract/artifact-schema.ts +110 -15
  21. package/src/contract/build.ts +1184 -613
  22. package/src/contract/compile.ts +44 -10
  23. package/src/contract/connect.ts +71 -5
  24. package/src/contract/context.ts +132 -23
  25. package/src/contract/declaration.ts +78 -63
  26. package/src/contract/defaults.ts +64 -58
  27. package/src/contract/define.ts +76 -124
  28. package/src/contract/file.ts +78 -0
  29. package/src/contract/group.ts +244 -37
  30. package/src/contract/index.ts +18 -0
  31. package/src/contract/kit.ts +206 -126
  32. package/src/contract/platform.ts +8 -9
  33. package/src/contract/redirect.ts +49 -0
  34. package/src/contract/schema-from-shape.ts +7 -0
  35. package/src/contract/schema-shape.ts +10 -0
  36. package/src/contract/snapshot-source.ts +5 -0
  37. package/src/contract/token.ts +89 -0
  38. package/src/contract/webhook-binding.ts +26 -0
  39. package/src/contract/webhook-scheme.ts +32 -0
  40. package/src/files/handle.ts +22 -0
  41. package/src/files/kinds.ts +454 -0
  42. package/src/files/scan.ts +15 -9
  43. package/src/files/streams.ts +111 -0
  44. package/src/honeypot.ts +7 -0
  45. package/src/implement/anonymous.ts +18 -42
  46. package/src/implement/boot.ts +177 -159
  47. package/src/implement/bounded.ts +23 -0
  48. package/src/implement/browser-config.ts +4 -4
  49. package/src/implement/capability.ts +78 -2
  50. package/src/implement/{form-admission.ts → client-address.ts} +3 -38
  51. package/src/implement/dispatch.ts +810 -409
  52. package/src/implement/egress.ts +84 -0
  53. package/src/implement/faults.ts +11 -1
  54. package/src/implement/index.ts +3 -2
  55. package/src/implement/invocation.ts +135 -35
  56. package/src/implement/params.ts +88 -0
  57. package/src/implement/principal.ts +7 -0
  58. package/src/implement/public.ts +142 -0
  59. package/src/implement/tokens.ts +219 -0
  60. package/src/implement/types.ts +24 -3
  61. package/src/implement/urls.ts +78 -0
  62. package/src/implement/verifiers.ts +299 -0
  63. package/src/implement/webhook.ts +136 -55
  64. package/src/implement/workload.ts +16 -14
  65. package/src/index.ts +3 -0
  66. package/src/jobs/mail.ts +98 -10
  67. package/src/jobs/runner.ts +103 -71
  68. package/src/jobs/scrub.ts +4 -1
  69. package/src/jobs/types.ts +19 -2
  70. package/src/react/hooks.ts +2 -2
  71. package/src/react/provider.tsx +2 -2
  72. package/src/testing/dev.ts +2 -2
  73. package/src/testing/endpoint-access.ts +167 -128
  74. package/src/testing/kit.ts +79 -24
  75. package/src/testing/policy-isolation.ts +55 -29
  76. package/src/testing/proxy.ts +6 -2
  77. package/src/testing/sample-file.ts +30 -0
  78. package/src/client/files.ts +0 -135
  79. package/src/contract/file-schemas.ts +0 -25
  80. package/src/files/collections.ts +0 -848
@@ -2,7 +2,7 @@
2
2
  // once described. `buildArtifact` reads only this shape, so a reader that produces it from source (B1) and
3
3
  // `compileContract` normalize through the same code.
4
4
 
5
- import type { ChallengeProvider, CspSource, MailTemplate, RpcCaller } from '@fabrika/auth-core'
5
+ import type { CspSource, MailTemplate, RpcCaller } from '@fabrika/auth-core'
6
6
 
7
7
  export interface ContractDeclaration<S> {
8
8
  readonly app: string
@@ -10,11 +10,13 @@ export interface ContractDeclaration<S> {
10
10
  readonly actions: Readonly<Record<string, string>>
11
11
  readonly roles: Readonly<Record<string, RoleDeclaration>>
12
12
  readonly mounts: Readonly<Record<string, MountDeclaration<S>>>
13
- readonly files: Readonly<Record<string, FilesDeclaration>>
13
+ readonly files: Readonly<Record<string, FileKindDeclaration>>
14
14
  readonly jobs: Readonly<Record<string, CronDeclaration>>
15
15
  readonly exports: Readonly<Record<string, ExportDeclaration<S>>>
16
16
  readonly imports: Readonly<Record<string, ImportDeclaration>>
17
17
  readonly csp: CspDeclaration
18
+ /** The token purposes of `defineApp`'s options, by purpose; absent without any. */
19
+ readonly tokens?: Readonly<Record<string, TokenDeclaration<S>>>
18
20
  /** A language IAM speaks; `buildArtifact` refuses another value. */
19
21
  readonly locale?: string
20
22
  readonly displayName?: string
@@ -30,7 +32,7 @@ export interface ContractDeclaration<S> {
30
32
  * The keys of the object `defineContract`'s callback returns that are sections, not mounts. Every other key is a
31
33
  * mount. A reader of source splits the object the same way.
32
34
  */
33
- export const SURFACE_SECTIONS = ['files', 'jobs', 'exports', 'imports', 'csp'] as const
35
+ export const SURFACE_SECTIONS = ['files', 'jobs', 'exports', 'imports', 'csp', 'tokens'] as const
34
36
 
35
37
  export type SurfaceSection = (typeof SURFACE_SECTIONS)[number]
36
38
 
@@ -39,7 +41,7 @@ export type SurfaceSection = (typeof SURFACE_SECTIONS)[number]
39
41
  * records every other key of its options in `unknownOptions`, so an option can never replace what the builder set;
40
42
  * `buildArtifact` refuses a non-empty `unknownOptions`. A reader of source fills it the same way.
41
43
  */
42
- export type Builder = 'procedure' | 'rpc' | 'link' | 'spa' | 'form' | 'webhook' | 'files' | 'cron' | 'export' | 'attachment'
44
+ export type Builder = 'procedure' | 'rpc' | 'link' | 'spa' | 'webhook' | 'fileKind' | 'cron' | 'export' | 'attachment'
43
45
 
44
46
  export interface BuilderFields {
45
47
  readonly options: readonly string[]
@@ -47,14 +49,13 @@ export interface BuilderFields {
47
49
  }
48
50
 
49
51
  export const BUILDER_FIELDS: Readonly<Record<Builder, BuilderFields>> = {
50
- procedure: { options: ['input', 'output', 'access', 'crossOrigin'], owned: ['~kind', 'type'] },
52
+ procedure: { options: ['input', 'output', 'access', 'actions', 'admission', 'limits', 'http'], owned: ['~kind', 'type'] },
51
53
  rpc: { options: ['callers'], owned: ['kind', 'prefix', 'procedures'] },
52
54
  link: { options: ['spa', 'dimension', 'grants', 'issuedBy', 'maxDays', 'procedures'], owned: ['kind', 'prefix'] },
53
- spa: { options: ['dir', 'access', 'public'], owned: ['kind', 'prefix'] },
54
- form: { options: ['input', 'appendTo', 'maxBytes', 'rate', 'honeypot', 'challenge', 'then', 'crossOrigin', 'files'], owned: ['kind', 'path'] },
55
- webhook: { options: ['scheme', 'boundTo', 'actions', 'input', 'maxBytes'], owned: ['kind', 'path'] },
56
- files: { options: ['owner', 'read', 'write', 'delete', 'maxBytes', 'types', 'maxPerOwner', 'link', 'scan'], owned: ['kind'] },
57
- cron: { options: ['actions', 'scope', 'timeout', 'retries'], owned: ['kind', 'schedule'] },
55
+ spa: { options: ['dir', 'access', 'browserConfig'], owned: ['kind', 'prefix'] },
56
+ webhook: { options: ['scheme', 'boundTo', 'actions', 'input', 'limits'], owned: ['kind', 'path'] },
57
+ fileKind: { options: ['types', 'maxBytes', 'scan'], owned: ['kind'] },
58
+ cron: { options: ['actions', 'limits', 'retries'], owned: ['kind', 'schedule'] },
58
59
  export: { options: ['version', 'consumers', 'audience', 'procedures'], owned: ['kind'] },
59
60
  attachment: { options: ['format', 'filename', 'row', 'maxBytes', 'maxRows'], owned: [] },
60
61
  }
@@ -69,7 +70,7 @@ export function unknownOptions(options: object, builder: Builder): string[] {
69
70
  * The off-road fields a site of the staged kit declares beside its builder's options. The kit copies them into
70
71
  * `declared`; `buildArtifact` checks which of them the site may carry.
71
72
  */
72
- export const DECLARED_FIELDS = ['sql', 'unscoped', 'connect', 'issues', 'then', 'mailer'] as const
73
+ export const DECLARED_FIELDS = ['sql', 'unscoped', 'connect', 'issues', 'mailer', 'kicks', 'binds', 'signs', 'egress'] as const
73
74
 
74
75
  /** The keys of a kit site's `options` that are neither `builder`'s option fields nor a declared field. */
75
76
  export function kitUnknownOptions(options: object, builder: Builder): string[] {
@@ -77,7 +78,7 @@ export function kitUnknownOptions(options: object, builder: Builder): string[] {
77
78
  }
78
79
 
79
80
  /**
80
- * The reach a site of the staged kit declares, as written (`sql`, `unscoped`, `connect`, `issues`, `then`, `mailer`): only the fields
81
+ * The reach a site of the staged kit declares, as written (`sql`, `unscoped`, `connect`, `issues`, `mailer`, `kicks`, `binds`, `signs`, `egress`): only the fields
81
82
  * its options carry. Unchecked here: the static reader copies what the source holds, and `buildArtifact` parses it.
82
83
  */
83
84
  export interface DeclaredDeclaration {
@@ -85,8 +86,11 @@ export interface DeclaredDeclaration {
85
86
  readonly unscoped?: unknown
86
87
  readonly connect?: unknown
87
88
  readonly issues?: unknown
88
- readonly then?: unknown
89
89
  readonly mailer?: unknown
90
+ readonly kicks?: unknown
91
+ readonly binds?: unknown
92
+ readonly signs?: unknown
93
+ readonly egress?: unknown
90
94
  }
91
95
 
92
96
  /** The declared fields of a kit site's options, each one only when the options carry it. */
@@ -96,11 +100,20 @@ export function declaredOf(options: DeclaredDeclaration): DeclaredDeclaration {
96
100
  ...(options.unscoped === undefined ? {} : { unscoped: options.unscoped }),
97
101
  ...(options.connect === undefined ? {} : { connect: options.connect }),
98
102
  ...(options.issues === undefined ? {} : { issues: options.issues }),
99
- ...(options.then === undefined ? {} : { then: options.then }),
100
103
  ...(options.mailer === undefined ? {} : { mailer: options.mailer }),
104
+ ...(options.kicks === undefined ? {} : { kicks: options.kicks }),
105
+ ...(options.binds === undefined ? {} : { binds: options.binds }),
106
+ ...(options.signs === undefined ? {} : { signs: options.signs }),
107
+ ...(options.egress === undefined ? {} : { egress: options.egress }),
101
108
  }
102
109
  }
103
110
 
111
+ /** One token purpose as written: its claims schema and its longest lifetime; `buildArtifact` checks both. */
112
+ export interface TokenDeclaration<S> {
113
+ readonly claims: S
114
+ readonly maxDays: unknown
115
+ }
116
+
104
117
  /** Option keys outside the builder's option fields; `buildArtifact` refuses any. */
105
118
  interface OptionsRecord {
106
119
  readonly unknownOptions?: readonly string[]
@@ -128,12 +141,27 @@ export type AccessDeclaration =
128
141
  | { readonly action: string; readonly on: string; readonly from: string }
129
142
  | { readonly action: string; readonly creates: string }
130
143
  | { readonly action: string; readonly app: true }
144
+ | { readonly capabilities: Readonly<Record<string, CapabilityDeclaration>> }
145
+ /** Anyone calls it; its handler runs as the procedure's own workload, with its top-level `actions`. RPC only. */
146
+ | { readonly public: true }
147
+
148
+ /** One of several capabilities: an action in a dimension, listed `over` it or read from an input field. */
149
+ export type CapabilityDeclaration =
150
+ | { readonly action: string; readonly over: string }
151
+ | { readonly action: string; readonly in: string; readonly from: string }
152
+ | { readonly action: string; readonly on: string; readonly from: string }
131
153
 
132
154
  /**
133
155
  * An export procedure's access: an action in a dimension, never `member`, because the consumer's key holds exactly the
134
- * actions, and never `app`, because a v1 export declares one dimension.
156
+ * actions, never `app`, because a v1 export declares one dimension, and never several capabilities.
135
157
  */
136
- export type ExportAccessDeclaration = Exclude<AccessDeclaration, { readonly member: true } | { readonly app: true }>
158
+ export type ExportAccessDeclaration = Exclude<
159
+ AccessDeclaration,
160
+ | { readonly member: true }
161
+ | { readonly app: true }
162
+ | { readonly capabilities: Readonly<Record<string, CapabilityDeclaration>> }
163
+ | { readonly public: true }
164
+ >
137
165
 
138
166
  export interface LinkAccessDeclaration {
139
167
  readonly action: string
@@ -155,8 +183,14 @@ export interface ProcedureDeclaration<S, Access = AccessDeclaration> extends Opt
155
183
  readonly input: S | null
156
184
  readonly output: S
157
185
  readonly access: Access
158
- /** `true` opens the procedure to the environment's listed origins, as written; `buildArtifact` refuses anything else. */
159
- readonly crossOrigin?: unknown
186
+ /** A public procedure's workload actions, as written: all its handler holds. Required there, refused elsewhere. */
187
+ readonly actions?: unknown
188
+ /** `{ origins?, rate?, challenge? }`, as written; `buildArtifact` merges it into the defaults. */
189
+ readonly admission?: unknown
190
+ /** `{ maxBytes?, timeoutMs? }`, as written; `buildArtifact` checks each against its ceiling. */
191
+ readonly limits?: unknown
192
+ /** `{ method?, query?, body?, cache? }`, as written; `buildArtifact` checks the HTTP shape's rules. */
193
+ readonly http?: unknown
160
194
  /** What a procedure of the staged kit declares off the paved road; a `defineContract` procedure has none. */
161
195
  readonly declared?: unknown
162
196
  /**
@@ -164,6 +198,13 @@ export interface ProcedureDeclaration<S, Access = AccessDeclaration> extends Opt
164
198
  * `output` is the array of rows the handler returns.
165
199
  */
166
200
  readonly attachment?: AttachmentDeclaration
201
+ /** Set by `describeContract` when the output is a `redirect()`: the procedure answers a redirect to `{ to }`. */
202
+ readonly redirect?: RedirectDeclaration
203
+ }
204
+
205
+ /** The origins a `redirect()` output lists, as written; `buildArtifact` checks each. */
206
+ export interface RedirectDeclaration {
207
+ readonly origins: readonly string[]
167
208
  }
168
209
 
169
210
  /**
@@ -187,14 +228,16 @@ export type MountDeclaration<S> =
187
228
  | LinkMountDeclaration<S>
188
229
  | SpaMountDeclaration
189
230
  | HealthMountDeclaration
190
- | FormMountDeclaration<S>
191
231
  | WebhookMountDeclaration<S>
192
232
 
193
233
  export interface RpcMountDeclaration<S> extends OptionsRecord {
194
234
  readonly kind: 'rpc'
195
235
  readonly prefix: string
196
- readonly callers: readonly [RpcCaller, ...RpcCaller[]]
236
+ /** The callers of its credentialed procedures; none when every procedure is public. */
237
+ readonly callers: readonly RpcCaller[]
197
238
  readonly procedures: ProcedureTreeDeclaration<S>
239
+ /** The kit's `c.rpc` options, as written; `buildArtifact` takes `'backward'` and `'unchecked'`. */
240
+ readonly compatibility?: unknown
198
241
  }
199
242
 
200
243
  export interface LinkMountDeclaration<S> extends OptionsRecord {
@@ -214,7 +257,7 @@ export interface SpaMountDeclaration extends OptionsRecord {
214
257
  readonly dir: string
215
258
  readonly access: SpaAccessDeclaration
216
259
  /** Application variables whose values the mount's pages read from `<prefix>/__fabrika-config.json`. */
217
- readonly public?: readonly string[]
260
+ readonly browserConfig?: readonly string[]
218
261
  }
219
262
 
220
263
  export interface HealthMountDeclaration {
@@ -222,57 +265,30 @@ export interface HealthMountDeclaration {
222
265
  readonly path: string
223
266
  }
224
267
 
225
- export interface FormMountDeclaration<S> extends OptionsRecord {
226
- readonly kind: 'form'
227
- readonly path: string
228
- readonly input: S
229
- readonly appendTo: string
230
- readonly maxBytes?: number
231
- /**
232
- * Submissions per minute from one client and accepted in all, and the bytes a form with files writes per hour;
233
- * `FORM_LIMITS` and `FORM_FILE_LIMITS` hold the defaults.
234
- */
235
- readonly rate?: { readonly perClient?: number; readonly perForm?: number; readonly bytesPerHour?: number }
236
- /** An input field that must stay empty. */
237
- readonly honeypot?: string
238
- /**
239
- * A challenge verified on the server; `secret` names the application variable that holds the provider's secret,
240
- * `siteKey` the one that holds the widget's public site key.
241
- */
242
- readonly challenge?: { readonly provider: ChallengeProvider; readonly secret: string; readonly siteKey?: string }
243
- /** The cron job of the contract that each accepted submission kicks. */
244
- readonly then?: string
245
- /** `true` (the environment's listed origins) or `'*'` (any origin), as written; `buildArtifact` refuses anything else. */
246
- readonly crossOrigin?: unknown
247
- /** The file fields, by name: `{ collection, required? }` each, as written; `buildArtifact` checks them. */
248
- readonly files?: unknown
249
- }
250
-
251
268
  export interface WebhookMountDeclaration<S> extends OptionsRecord {
252
269
  readonly kind: 'webhook'
253
270
  readonly path: string
254
- readonly scheme: 'github-sha256'
271
+ /** A preset or a verifier shape, as written; `buildArtifact` checks it. */
272
+ readonly scheme: unknown
255
273
  /** A dimension, or `app` for the whole application. */
256
274
  readonly boundTo: string
257
275
  readonly actions: readonly string[]
258
276
  readonly input: S
259
- readonly maxBytes?: number
277
+ /** `{ maxBytes?, timeoutMs? }`, as written; `buildArtifact` checks each against its ceiling. */
278
+ readonly limits?: unknown
260
279
  /** What a webhook of the staged kit declares off the paved road. */
261
280
  readonly declared?: unknown
262
281
  }
263
282
 
264
- export interface FilesDeclaration extends OptionsRecord {
265
- readonly kind: 'files'
266
- readonly owner: string
267
- readonly read: string
268
- readonly write: string
269
- readonly delete: string
270
- readonly maxBytes: number
283
+ /**
284
+ * A file kind (`c.files({ types, maxBytes, scan? })`, no `owner`): a class of stored files a table's file column holds.
285
+ * It has no owner, no actions and no endpoint.
286
+ */
287
+ export interface FileKindDeclaration extends OptionsRecord {
288
+ readonly kind: 'file-kind'
271
289
  readonly types: readonly string[]
272
- readonly maxPerOwner?: number
273
- /** `true`: every link mount bound to the owner's dimension serves the files; a list: the named link mounts only. */
274
- readonly link?: boolean | readonly string[]
275
- /** Every upload waits `quarantined` until the scan pass gives it a clean verdict. */
290
+ readonly maxBytes: number
291
+ /** Every file waits `quarantined` until the scan gives it a clean verdict; a byte consumer answers 409 until then. */
276
292
  readonly scan?: boolean
277
293
  }
278
294
 
@@ -280,9 +296,8 @@ export interface CronDeclaration extends OptionsRecord {
280
296
  readonly kind: 'cron'
281
297
  readonly schedule: string
282
298
  readonly actions: readonly string[]
283
- /** A dimension, or `app` for the whole application. */
284
- readonly scope: string
285
- readonly timeout?: string
299
+ /** `{ timeoutMs? }`, as written; `buildArtifact` checks it against its ceiling. */
300
+ readonly limits?: unknown
286
301
  readonly retries?: number
287
302
  /** What a job of the staged kit declares off the paved road. */
288
303
  readonly declared?: unknown
@@ -6,73 +6,85 @@ import type { BodyAdmission, ChallengeProvider } from '@fabrika/auth-core'
6
6
  export const CONTRACT_DEFAULTS = {
7
7
  /** A failed job run is not retried unless the job asks for it: a retry repeats work. */
8
8
  jobRetries: 0,
9
- formMaxBytes: 16 * 1024,
10
- webhookMaxBytes: 1024 * 1024,
11
- fileMaxPerOwner: null,
12
- fileLink: false,
13
9
  importTimeoutMs: null,
14
10
  } as const
15
11
 
16
12
  /**
17
- * A form's admission, per minute and shared by every instance of the application: the requests it admits from one
18
- * client (`X-Fabrika-Client-Ip`) and the submissions it accepts in all. A form may declare its own `rate`, up to
19
- * `maxPerMinute` each.
13
+ * A procedure's bounds: its body (`maxBytes`, without files) and its handler's time (`timeoutMs`, counted after the last
14
+ * body byte), each with its default and its ceiling, and the most calls a bucket admits per minute.
20
15
  */
21
- export const FORM_LIMITS = {
22
- perClient: 10,
23
- perForm: 100,
16
+ export const PROCEDURE_LIMITS = {
17
+ maxBytes: 64 * 1024,
18
+ maxMaxBytes: 1024 * 1024,
19
+ timeoutMs: 10_000,
20
+ maxTimeoutMs: 60_000,
24
21
  maxPerMinute: 10_000,
22
+ maxBytesPerHour: 64 * 1024 * 1024 * 1024,
23
+ /** The body of a procedure with file fields: its default adds each field's files at its kind's `maxBytes`. */
24
+ maxFileBodyBytes: 48 * 1024 * 1024,
25
+ /** The files one request carries, at most: each field's `.max(n)` together. */
26
+ maxFiles: 20,
27
+ /** The headers of one part of a multipart body, at most. */
28
+ maxPartHeaderBytes: 8 * 1024,
25
29
  } as const
26
30
 
27
- /** The input field that carries a form's challenge token; a form with a challenge declares it as a string. */
28
- export const FORM_CHALLENGE_FIELD = 'challengeToken'
29
-
30
31
  /**
31
- * A form that takes files is `multipart/form-data`: its input travels as JSON in the first part, named `input`, and each
32
- * file in a part named after its field. The proxy admits only that type for such a form.
32
+ * A procedure with file fields is `multipart/form-data`: its JSON input travels in the first part, named `input`, and
33
+ * each file in a part named after its field. A field of several files sends one part per file, under its name.
33
34
  */
34
- export { FORM_MULTIPART_TYPE } from '@fabrika/auth-core'
35
- export const FORM_INPUT_PART = 'input'
35
+ export const FILE_INPUT_PART = 'input'
36
36
 
37
37
  /**
38
- * The bounds of a form's files: at most `maxFields` file fields, a file each; a received file waits `holdMs` for the
39
- * job that drains the inbox to attach it, then the sweep deletes it; a submission's body must arrive within
40
- * `timeoutMs`; a part's headers hold at most `maxPartHeaderBytes`.
38
+ * A webhook's bounds: a provider's body, by default the most a procedure without files takes, since providers send
39
+ * whole objects; up to what a procedure with file fields takes, since an inbound e-mail carries its attachments in the
40
+ * body for the handler to `put`; and its handler's time, a procedure's.
41
41
  */
42
- export const FORM_FILE_LIMITS = {
43
- maxFields: 4,
44
- holdMs: 7 * 24 * 60 * 60 * 1000,
45
- timeoutMs: 5 * 60 * 1000,
46
- maxPartHeaderBytes: 8 * 1024,
47
- /**
48
- * A form with files accepts this many submissions per minute unless it declares `rate.perForm`, and its files write
49
- * at most `bytesPerHour` per hour unless it declares `rate.bytesPerHour` (at most `maxBytesPerHour`). Each file
50
- * reserves its collection's `maxBytes` before its first byte is written, and gives back what it did not use.
51
- */
52
- perForm: 20,
53
- bytesPerHour: 512 * 1024 * 1024,
54
- maxBytesPerHour: 64 * 1024 * 1024 * 1024,
42
+ export const WEBHOOK_LIMITS = {
43
+ maxBytes: PROCEDURE_LIMITS.maxMaxBytes,
44
+ maxMaxBytes: PROCEDURE_LIMITS.maxFileBodyBytes,
45
+ timeoutMs: PROCEDURE_LIMITS.timeoutMs,
55
46
  } as const
56
47
 
48
+ /** A job attempt's time limit (`limits.timeoutMs`) and its ceiling, which keeps a lease and a timer within a day. */
49
+ export const JOB_LIMITS = {
50
+ timeoutMs: 15 * 60_000,
51
+ maxTimeoutMs: 24 * 60 * 60_000,
52
+ } as const
53
+
54
+ /** A public mutation's buckets per minute unless it declares its own: per client and for the endpoint. */
55
+ export const PUBLIC_MUTATION_RATE = { perClient: 10, perEndpoint: 100 } as const
56
+
57
+ /** What Turnstile admits as a widget's `action`: at most 32 letters, digits, `_` and `-`. */
58
+ export const CHALLENGE_ACTION = /^[A-Za-z0-9_-]{1,32}$/
59
+
60
+ /**
61
+ * The challenge widget's `action` of a public procedure that declares none (`challenge.action`): its endpoint id with
62
+ * `_` for `.`.
63
+ */
64
+ export function challengeAction(endpoint: string): string {
65
+ return endpoint.replaceAll('.', '_')
66
+ }
67
+
68
+ /** The input field that carries a challenge token; a procedure with a challenge declares it as a string. */
69
+ export const CHALLENGE_FIELD = 'challengeToken'
70
+
71
+ /** A procedure with file fields reads `multipart/form-data`; the proxy admits that type for its row. */
72
+ export { FORM_MULTIPART_TYPE } from '@fabrika/auth-core'
73
+
57
74
  /** Each challenge provider's fixed browser origins: its widget's script and its frame. */
58
75
  export const CHALLENGE_ORIGINS: Readonly<Record<ChallengeProvider, { readonly scriptSrc: readonly string[]; readonly frameSrc: readonly string[] }>> =
59
76
  {
60
77
  turnstile: { scriptSrc: ['https://challenges.cloudflare.com'], frameSrc: ['https://challenges.cloudflare.com'] },
61
78
  }
62
79
 
63
- /** Generated file endpoints live under this prefix, for signed-in people. */
64
- export const FILES_PREFIX = '/api'
65
-
66
80
  /**
67
- * The bounds of a file collection: the largest file a `scan: true` collection admits (the scanner reads at most this
68
- * much of one file, so a larger one could never be given a clean verdict), and the pages of `files.<name>.list`.
81
+ * The bounds of a file kind: the largest file a `scan: true` kind admits (the scanner reads at most this much of one
82
+ * file, so a larger one could never be given a clean verdict).
69
83
  */
70
84
  export const FILE_LIMITS = {
71
85
  maxScannedBytes: 100 * 1024 * 1024,
72
- /** Sniffable types a `scan: true` collection refuses: archives other than ZIP, whose contents the framework does not check. */
86
+ /** Sniffable types a `scan: true` kind refuses: archives other than ZIP, whose contents the framework does not check. */
73
87
  unscannedTypes: ['application/gzip'] as readonly string[],
74
- listPageDefault: 50,
75
- listPageMax: 100,
76
88
  } as const
77
89
 
78
90
  /**
@@ -116,15 +128,6 @@ export const LINK_API_SEGMENT = 'api'
116
128
  /** The path segment a webhook appends for the binding a delivery addresses. */
117
129
  export const WEBHOOK_BINDING_PARAMETER = 'bindingId'
118
130
 
119
- /** Generated webhook binding endpoints (`webhooks.<name>.bind`, `.unbind`) live under this prefix, for signed-in people. */
120
- export const WEBHOOKS_PREFIX = '/api'
121
-
122
- /** The input field of a binding endpoint that names the scope value the binding reaches. */
123
- export const WEBHOOK_SCOPE_FIELD = 'scope'
124
-
125
- /** A GitHub account a binding accepts: an App installation or a repository, by numeric id. */
126
- export const WEBHOOK_ACCOUNT_PATTERN = '^(?:installation|repository):[1-9][0-9]{0,19}$'
127
-
128
131
  /**
129
132
  * The share SPA's bootstrap, served by the framework under the link prefix and loaded before any other script: it
130
133
  * takes the key out of the URL fragment and keeps it in a closure behind `SHARE_KEY_GLOBAL`.
@@ -168,6 +171,12 @@ export const GENERATED_LINK = 'fabrika:link'
168
171
  */
169
172
  export const GENERATED_ROW_PREFIX = 'fabrika:row:'
170
173
 
174
+ /**
175
+ * The prefix of the value a generated endpoint access case sends in a file field, followed by the field's file kind:
176
+ * `fabrika:file:attachments`. The endpoint access kit sends a small file of the kind's first type it can write in its place.
177
+ */
178
+ export const GENERATED_FILE_PREFIX = 'fabrika:file:'
179
+
171
180
  export const JSON_ADMISSION: BodyAdmission = { contentTypes: ['application/json'], maxBytes: null }
172
181
 
173
182
  /** `/api` + `notes.list` → `/api/notes.list`; a root prefix does not double the `/`. */
@@ -176,13 +185,10 @@ export function joinPath(prefix: string, rest: string): string {
176
185
  }
177
186
 
178
187
  /**
179
- * The paths of the endpoints of collection `name` under `base` (`FILES_PREFIX`, or a link mount's API prefix): the
180
- * upload takes `/:owner` after its path for a signed-in caller, and the download `/:fileId`.
188
+ * The path of an RPC mount's procedure: its own `http.path` when it declares one (a public procedure only, which
189
+ * `buildArtifact` checks), else `<prefix>/<dotted.path>`. The typed client calls the path the artifact's row carries.
181
190
  */
182
- export function fileCollectionPaths(
183
- base: string,
184
- name: string,
185
- ): { readonly upload: string; readonly download: string; readonly list: string; readonly remove: string } {
186
- const at = joinPath(base, `files.${name}`)
187
- return { upload: `${at}.upload`, download: at, list: `${at}.list`, remove: `${at}.remove` }
191
+ export function procedurePath(prefix: string, dotted: string, http: unknown): string {
192
+ const own: unknown = typeof http === 'object' && http !== null ? Reflect.get(http, 'path') : undefined
193
+ return typeof own === 'string' ? own : joinPath(prefix, dotted)
188
194
  }