@fabrika/app 0.3.2 → 0.4.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 (114) hide show
  1. package/README.md +78 -90
  2. package/package.json +68 -6
  3. package/skill/SKILL.md +319 -0
  4. package/skill/references/app-setup.md +364 -0
  5. package/skill/references/dev.md +67 -0
  6. package/skill/references/endpoint.md +627 -0
  7. package/skill/references/errors.md +108 -0
  8. package/skill/references/files.md +335 -0
  9. package/skill/references/jobs.md +169 -0
  10. package/skill/references/public-intake.md +1495 -0
  11. package/skill/references/spa.md +741 -0
  12. package/skill/references/start.md +179 -0
  13. package/skill/references/table-and-migration.md +264 -0
  14. package/skill/references/tests.md +305 -0
  15. package/skill/references/webhooks-links-connectors.md +190 -0
  16. package/src/agent-skill.ts +77 -0
  17. package/src/build.ts +53 -0
  18. package/src/bun.ts +302 -3
  19. package/src/client/caller.ts +24 -0
  20. package/src/client/contract.ts +309 -0
  21. package/src/client/files.ts +135 -0
  22. package/src/client/index.ts +19 -0
  23. package/src/client/rpc.ts +47 -0
  24. package/src/client/share.ts +14 -0
  25. package/src/{client.ts → client/transport.ts} +58 -61
  26. package/src/cloudflare.ts +139 -3
  27. package/src/contract/artifact-schema.ts +317 -0
  28. package/src/contract/attachment.ts +74 -0
  29. package/src/contract/build.ts +1936 -0
  30. package/src/contract/compile.ts +143 -0
  31. package/src/contract/connect.ts +170 -0
  32. package/src/contract/context.ts +437 -0
  33. package/src/contract/declaration.ts +310 -0
  34. package/src/contract/defaults.ts +156 -0
  35. package/src/contract/define.ts +567 -0
  36. package/src/contract/file-schemas.ts +25 -0
  37. package/src/contract/group.ts +263 -0
  38. package/src/contract/handled.ts +50 -0
  39. package/src/contract/index.ts +34 -0
  40. package/src/contract/kit.ts +607 -0
  41. package/src/contract/platform.ts +113 -0
  42. package/src/contract/problems.ts +30 -0
  43. package/src/contract/schema-from-shape.ts +152 -0
  44. package/src/contract/schema-shape.ts +407 -0
  45. package/src/contract/snapshot-source.ts +168 -0
  46. package/src/contract/vocabulary.ts +50 -0
  47. package/src/disk-assets.ts +25 -0
  48. package/src/fetch.ts +10 -0
  49. package/src/files/archive.ts +292 -0
  50. package/src/files/clamscan.ts +253 -0
  51. package/src/files/collections.ts +669 -0
  52. package/src/files/handle.ts +77 -0
  53. package/src/files/scan.ts +308 -0
  54. package/src/files/sniff.ts +70 -0
  55. package/src/files/treatment.ts +48 -0
  56. package/src/from-row.ts +53 -0
  57. package/src/implement/anonymous.ts +102 -0
  58. package/src/implement/attachment.ts +143 -0
  59. package/src/implement/boot.ts +808 -0
  60. package/src/implement/bounded.ts +72 -0
  61. package/src/implement/browser-config.ts +54 -0
  62. package/src/implement/capability.ts +439 -0
  63. package/src/implement/challenge.ts +115 -0
  64. package/src/implement/connect.ts +386 -0
  65. package/src/implement/dispatch.ts +1085 -0
  66. package/src/implement/environment.ts +47 -0
  67. package/src/implement/failures.ts +116 -0
  68. package/src/implement/faults.ts +46 -0
  69. package/src/implement/form-admission.ts +120 -0
  70. package/src/implement/headers.ts +17 -0
  71. package/src/implement/implement.ts +81 -0
  72. package/src/implement/index.ts +60 -0
  73. package/src/implement/invocation.ts +346 -0
  74. package/src/implement/membership.ts +485 -0
  75. package/src/implement/principal.ts +99 -0
  76. package/src/implement/project.ts +76 -0
  77. package/src/implement/serve.ts +142 -0
  78. package/src/implement/types.ts +215 -0
  79. package/src/implement/webhook.ts +108 -0
  80. package/src/implement/workload.ts +160 -0
  81. package/src/implement/xlsx.ts +275 -0
  82. package/src/index.ts +12 -13
  83. package/src/jobs/report.ts +134 -0
  84. package/src/jobs/runner.ts +749 -0
  85. package/src/jobs/schedule.ts +129 -0
  86. package/src/jobs/types.ts +44 -0
  87. package/src/react/caller.tsx +60 -0
  88. package/src/react/errors.ts +82 -0
  89. package/src/react/hooks.ts +239 -0
  90. package/src/react/index.ts +31 -0
  91. package/src/react/provider.tsx +165 -0
  92. package/src/react/store.ts +215 -0
  93. package/src/react/types.ts +40 -0
  94. package/src/rpc/dispatcher.ts +4 -19
  95. package/src/service/define.ts +121 -0
  96. package/src/service/index.ts +21 -0
  97. package/src/service/serve.ts +99 -0
  98. package/src/testing/caller.ts +123 -0
  99. package/src/testing/dev.ts +73 -0
  100. package/src/testing/endpoint-access.ts +806 -0
  101. package/src/testing/index.ts +23 -0
  102. package/src/testing/isolation.ts +25 -0
  103. package/src/testing/kit.ts +552 -0
  104. package/src/testing/membership.ts +747 -0
  105. package/src/testing/policy-isolation.ts +827 -0
  106. package/src/testing/preload.ts +6 -0
  107. package/src/testing/proxy.ts +87 -0
  108. package/src/testing/runner.ts +61 -0
  109. package/src/testing/status.ts +19 -0
  110. package/src/undumpable.ts +42 -0
  111. package/src/app.ts +0 -76
  112. package/src/middleware.ts +0 -38
  113. package/src/router.ts +0 -149
  114. package/src/testing.ts +0 -119
@@ -0,0 +1,310 @@
1
+ // A contract as plain data, with each schema of type `S`: a zod schema in the evaluated contract, a `SchemaShape`
2
+ // once described. `buildArtifact` reads only this shape, so a reader that produces it from source (B1) and
3
+ // `compileContract` normalize through the same code.
4
+
5
+ import type { ChallengeProvider, CspSource, MailTemplate, RpcCaller } from '@fabrika/auth-core'
6
+
7
+ export interface ContractDeclaration<S> {
8
+ readonly app: string
9
+ readonly dimensions: Readonly<Record<string, DimensionDeclaration>>
10
+ readonly actions: Readonly<Record<string, string>>
11
+ readonly roles: Readonly<Record<string, RoleDeclaration>>
12
+ readonly mounts: Readonly<Record<string, MountDeclaration<S>>>
13
+ readonly files: Readonly<Record<string, FilesDeclaration>>
14
+ readonly jobs: Readonly<Record<string, CronDeclaration>>
15
+ readonly exports: Readonly<Record<string, ExportDeclaration<S>>>
16
+ readonly imports: Readonly<Record<string, ImportDeclaration>>
17
+ readonly csp: CspDeclaration
18
+ /** A language IAM speaks; `buildArtifact` refuses another value. */
19
+ readonly locale?: string
20
+ readonly displayName?: string
21
+ /** Message kind → locale → template; `buildArtifact` refuses an unknown kind or locale and a bad template. */
22
+ readonly mail?: Readonly<Record<string, Readonly<Record<string, MailTemplate>>>>
23
+ /** The actions that manage and list the application's own people. */
24
+ readonly members?: { readonly manage: string; readonly list: string }
25
+ /** `'labels'` only; `buildArtifact` refuses another value, and refuses it without `members`. */
26
+ readonly directory?: string
27
+ }
28
+
29
+ /**
30
+ * The keys of the object `defineContract`'s callback returns that are sections, not mounts. Every other key is a
31
+ * mount. A reader of source splits the object the same way.
32
+ */
33
+ export const SURFACE_SECTIONS = ['files', 'jobs', 'exports', 'imports', 'csp'] as const
34
+
35
+ export type SurfaceSection = (typeof SURFACE_SECTIONS)[number]
36
+
37
+ /**
38
+ * The option fields each builder takes, and the fields it sets itself. A builder copies only its option fields and
39
+ * records every other key of its options in `unknownOptions`, so an option can never replace what the builder set;
40
+ * `buildArtifact` refuses a non-empty `unknownOptions`. A reader of source fills it the same way.
41
+ */
42
+ export type Builder = 'procedure' | 'rpc' | 'link' | 'spa' | 'form' | 'webhook' | 'files' | 'cron' | 'export' | 'attachment'
43
+
44
+ export interface BuilderFields {
45
+ readonly options: readonly string[]
46
+ readonly owned: readonly string[]
47
+ }
48
+
49
+ export const BUILDER_FIELDS: Readonly<Record<Builder, BuilderFields>> = {
50
+ procedure: { options: ['input', 'output', 'access'], owned: ['~kind', 'type'] },
51
+ rpc: { options: ['callers'], owned: ['kind', 'prefix', 'procedures'] },
52
+ 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'], 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'] },
58
+ export: { options: ['version', 'consumers', 'audience', 'procedures'], owned: ['kind'] },
59
+ attachment: { options: ['format', 'filename', 'row', 'maxBytes', 'maxRows'], owned: [] },
60
+ }
61
+
62
+ /** The keys of `options` that are not `builder`'s option fields. */
63
+ export function unknownOptions(options: object, builder: Builder): string[] {
64
+ const declared = BUILDER_FIELDS[builder].options
65
+ return Object.keys(options).filter((key) => !declared.includes(key))
66
+ }
67
+
68
+ /**
69
+ * The off-road fields a site of the staged kit declares beside its builder's options. The kit copies them into
70
+ * `declared`; `buildArtifact` checks which of them the site may carry.
71
+ */
72
+ export const DECLARED_FIELDS = ['sql', 'unscoped', 'connect', 'issues'] as const
73
+
74
+ /** The keys of a kit site's `options` that are neither `builder`'s option fields nor a declared field. */
75
+ export function kitUnknownOptions(options: object, builder: Builder): string[] {
76
+ return unknownOptions(options, builder).filter((key) => !DECLARED_FIELDS.some((field) => field === key))
77
+ }
78
+
79
+ /**
80
+ * The reach a site of the staged kit declares, as written (`sql`, `unscoped`, `connect`, `issues`): only the fields
81
+ * its options carry. Unchecked here: the static reader copies what the source holds, and `buildArtifact` parses it.
82
+ */
83
+ export interface DeclaredDeclaration {
84
+ readonly sql?: unknown
85
+ readonly unscoped?: unknown
86
+ readonly connect?: unknown
87
+ readonly issues?: unknown
88
+ }
89
+
90
+ /** The declared fields of a kit site's options, each one only when the options carry it. */
91
+ export function declaredOf(options: DeclaredDeclaration): DeclaredDeclaration {
92
+ return {
93
+ ...(options.sql === undefined ? {} : { sql: options.sql }),
94
+ ...(options.unscoped === undefined ? {} : { unscoped: options.unscoped }),
95
+ ...(options.connect === undefined ? {} : { connect: options.connect }),
96
+ ...(options.issues === undefined ? {} : { issues: options.issues }),
97
+ }
98
+ }
99
+
100
+ /** Option keys outside the builder's option fields; `buildArtifact` refuses any. */
101
+ interface OptionsRecord {
102
+ readonly unknownOptions?: readonly string[]
103
+ }
104
+
105
+ export interface DimensionDeclaration {
106
+ readonly in?: string
107
+ readonly creator?: string
108
+ readonly members?: { readonly manage: string; readonly list: string }
109
+ /** `'labels'` only; `buildArtifact` refuses another value. */
110
+ readonly directory?: string
111
+ }
112
+
113
+ export interface RoleDeclaration {
114
+ readonly grants: readonly string[]
115
+ readonly member?: { readonly scope: string; readonly assignableBy: readonly string[] }
116
+ /** `true` only; `buildArtifact` refuses another value, and refuses it on a role without `member`. */
117
+ readonly external?: boolean
118
+ }
119
+
120
+ export type AccessDeclaration =
121
+ | { readonly member: true }
122
+ | { readonly action: string; readonly over: string }
123
+ | { readonly action: string; readonly in: string; readonly from: string }
124
+ | { readonly action: string; readonly on: string; readonly from: string }
125
+ | { readonly action: string; readonly creates: string }
126
+ | { readonly action: string; readonly app: true }
127
+
128
+ /**
129
+ * An export procedure's access: an action in a dimension, never `member`, because the consumer's key holds exactly the
130
+ * actions, and never `app`, because a v1 export declares one dimension.
131
+ */
132
+ export type ExportAccessDeclaration = Exclude<AccessDeclaration, { readonly member: true } | { readonly app: true }>
133
+
134
+ export interface LinkAccessDeclaration {
135
+ readonly action: string
136
+ }
137
+
138
+ /** `{ public: true }` serves the SPA to anyone, with no session: an anonymous `public-spa` row. */
139
+ export type SpaAccessDeclaration =
140
+ | { readonly member: true }
141
+ | { readonly action: string; readonly over: string }
142
+ | { readonly action: string; readonly app: true }
143
+ | { readonly public: true }
144
+
145
+ export type ProcedureType = 'query' | 'mutation'
146
+
147
+ export interface ProcedureDeclaration<S, Access = AccessDeclaration> extends OptionsRecord {
148
+ readonly '~kind': 'procedure'
149
+ readonly type: ProcedureType
150
+ /** `null` when the procedure takes no input. */
151
+ readonly input: S | null
152
+ readonly output: S
153
+ readonly access: Access
154
+ /** What a procedure of the staged kit declares off the paved road; a `defineContract` procedure has none. */
155
+ readonly declared?: unknown
156
+ /**
157
+ * Set by `describeContract` when the output is an `attachment()`: the procedure answers a file, and its described
158
+ * `output` is the array of rows the handler returns.
159
+ */
160
+ readonly attachment?: AttachmentDeclaration
161
+ }
162
+
163
+ /**
164
+ * The file an `attachment()` output declares beside its row schema, as written; `buildArtifact` checks each field and
165
+ * applies the default `maxRows`.
166
+ */
167
+ export interface AttachmentDeclaration extends OptionsRecord {
168
+ readonly format: string
169
+ readonly filename: string
170
+ readonly maxBytes: number
171
+ readonly maxRows?: number
172
+ }
173
+
174
+ /** Procedures by name; a nested object groups them, and the path joins the names with `.`. */
175
+ export interface ProcedureTreeDeclaration<S, Access = AccessDeclaration> {
176
+ readonly [name: string]: ProcedureDeclaration<S, Access> | ProcedureTreeDeclaration<S, Access>
177
+ }
178
+
179
+ export type MountDeclaration<S> =
180
+ | RpcMountDeclaration<S>
181
+ | LinkMountDeclaration<S>
182
+ | SpaMountDeclaration
183
+ | HealthMountDeclaration
184
+ | FormMountDeclaration<S>
185
+ | WebhookMountDeclaration<S>
186
+
187
+ export interface RpcMountDeclaration<S> extends OptionsRecord {
188
+ readonly kind: 'rpc'
189
+ readonly prefix: string
190
+ readonly callers: readonly [RpcCaller, ...RpcCaller[]]
191
+ readonly procedures: ProcedureTreeDeclaration<S>
192
+ }
193
+
194
+ export interface LinkMountDeclaration<S> extends OptionsRecord {
195
+ readonly kind: 'link'
196
+ readonly prefix: string
197
+ readonly spa: string
198
+ readonly dimension: string
199
+ readonly grants: readonly string[]
200
+ readonly issuedBy: string
201
+ readonly maxDays: number
202
+ readonly procedures: ProcedureTreeDeclaration<S, LinkAccessDeclaration>
203
+ }
204
+
205
+ export interface SpaMountDeclaration extends OptionsRecord {
206
+ readonly kind: 'spa'
207
+ readonly prefix: string
208
+ readonly dir: string
209
+ readonly access: SpaAccessDeclaration
210
+ /** Application variables whose values the mount's pages read from `<prefix>/__fabrika-config.json`. */
211
+ readonly public?: readonly string[]
212
+ }
213
+
214
+ export interface HealthMountDeclaration {
215
+ readonly kind: 'health'
216
+ readonly path: string
217
+ }
218
+
219
+ export interface FormMountDeclaration<S> extends OptionsRecord {
220
+ readonly kind: 'form'
221
+ readonly path: string
222
+ readonly input: S
223
+ readonly appendTo: string
224
+ readonly maxBytes?: number
225
+ /** Submissions per minute from one client and accepted in all; `FORM_LIMITS` holds the defaults. */
226
+ readonly rate?: { readonly perClient?: number; readonly perForm?: number }
227
+ /** An input field that must stay empty. */
228
+ readonly honeypot?: string
229
+ /**
230
+ * A challenge verified on the server; `secret` names the application variable that holds the provider's secret,
231
+ * `siteKey` the one that holds the widget's public site key.
232
+ */
233
+ readonly challenge?: { readonly provider: ChallengeProvider; readonly secret: string; readonly siteKey?: string }
234
+ /** The cron job of the contract that each accepted submission kicks. */
235
+ readonly then?: string
236
+ }
237
+
238
+ export interface WebhookMountDeclaration<S> extends OptionsRecord {
239
+ readonly kind: 'webhook'
240
+ readonly path: string
241
+ readonly scheme: 'github-sha256'
242
+ /** A dimension, or `app` for the whole application. */
243
+ readonly boundTo: string
244
+ readonly actions: readonly string[]
245
+ readonly input: S
246
+ readonly maxBytes?: number
247
+ /** What a webhook of the staged kit declares off the paved road. */
248
+ readonly declared?: unknown
249
+ }
250
+
251
+ export interface FilesDeclaration extends OptionsRecord {
252
+ readonly kind: 'files'
253
+ readonly owner: string
254
+ readonly read: string
255
+ readonly write: string
256
+ readonly delete: string
257
+ readonly maxBytes: number
258
+ readonly types: readonly string[]
259
+ readonly maxPerOwner?: number
260
+ /** `true`: every link mount bound to the owner's dimension serves the files; a list: the named link mounts only. */
261
+ readonly link?: boolean | readonly string[]
262
+ /** Every upload waits `quarantined` until the scan pass gives it a clean verdict. */
263
+ readonly scan?: boolean
264
+ }
265
+
266
+ export interface CronDeclaration extends OptionsRecord {
267
+ readonly kind: 'cron'
268
+ readonly schedule: string
269
+ readonly actions: readonly string[]
270
+ /** A dimension, or `app` for the whole application. */
271
+ readonly scope: string
272
+ readonly timeout?: string
273
+ readonly retries?: number
274
+ /** What a job of the staged kit declares off the paved road. */
275
+ readonly declared?: unknown
276
+ }
277
+
278
+ export interface ExportDeclaration<S> extends OptionsRecord {
279
+ readonly kind: 'export'
280
+ readonly version: number
281
+ readonly consumers: readonly string[]
282
+ readonly audience: string
283
+ readonly procedures: ProcedureTreeDeclaration<S, ExportAccessDeclaration>
284
+ }
285
+
286
+ export interface ImportDeclaration {
287
+ readonly app: string
288
+ readonly export: string
289
+ readonly version: number
290
+ readonly digest: string
291
+ readonly procedures: readonly string[]
292
+ readonly usedBy: readonly string[]
293
+ readonly timeoutMs?: number
294
+ }
295
+
296
+ export interface CspDeclaration {
297
+ readonly connectSrc?: readonly CspSource[]
298
+ readonly imgSrc?: readonly CspSource[]
299
+ readonly frameAncestors?: readonly CspSource[]
300
+ readonly scriptSrc?: readonly CspSource[]
301
+ readonly frameSrc?: readonly CspSource[]
302
+ readonly styleSrc?: readonly CspSource[]
303
+ readonly fontSrc?: readonly CspSource[]
304
+ }
305
+
306
+ export function isProcedure<S, Access>(
307
+ node: ProcedureDeclaration<S, Access> | ProcedureTreeDeclaration<S, Access>,
308
+ ): node is ProcedureDeclaration<S, Access> {
309
+ return node['~kind'] === 'procedure'
310
+ }
@@ -0,0 +1,156 @@
1
+ // Every default and every generated path of the contract, in one place. `buildArtifact` applies them; a reader of
2
+ // source reaches them only through `buildArtifact`, so both producers emit the same artifact.
3
+
4
+ import type { BodyAdmission, ChallengeProvider } from '@fabrika/auth-core'
5
+
6
+ export const CONTRACT_DEFAULTS = {
7
+ /** A failed job run is not retried unless the job asks for it: a retry repeats work. */
8
+ jobRetries: 0,
9
+ formMaxBytes: 16 * 1024,
10
+ webhookMaxBytes: 1024 * 1024,
11
+ fileMaxPerOwner: null,
12
+ fileLink: false,
13
+ importTimeoutMs: null,
14
+ } as const
15
+
16
+ /**
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.
20
+ */
21
+ export const FORM_LIMITS = {
22
+ perClient: 10,
23
+ perForm: 100,
24
+ maxPerMinute: 10_000,
25
+ } as const
26
+
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
+ /** Each challenge provider's fixed browser origins: its widget's script and its frame. */
31
+ export const CHALLENGE_ORIGINS: Readonly<Record<ChallengeProvider, { readonly scriptSrc: readonly string[]; readonly frameSrc: readonly string[] }>> =
32
+ {
33
+ turnstile: { scriptSrc: ['https://challenges.cloudflare.com'], frameSrc: ['https://challenges.cloudflare.com'] },
34
+ }
35
+
36
+ /** Generated file endpoints live under this prefix, for signed-in people. */
37
+ export const FILES_PREFIX = '/api'
38
+
39
+ /**
40
+ * The bounds of a file collection: the largest file a `scan: true` collection admits (the scanner reads at most this
41
+ * much of one file, so a larger one could never be given a clean verdict), and the pages of `files.<name>.list`.
42
+ */
43
+ export const FILE_LIMITS = {
44
+ maxScannedBytes: 100 * 1024 * 1024,
45
+ /** Sniffable types a `scan: true` collection refuses: archives other than ZIP, whose contents the framework does not check. */
46
+ unscannedTypes: ['application/gzip'] as readonly string[],
47
+ listPageDefault: 50,
48
+ listPageMax: 100,
49
+ } as const
50
+
51
+ /**
52
+ * The bounds of an `attachment()` output. `maxBytes` and `maxRows` are each attachment's own, at most these; a cell holds
53
+ * at most `maxCellLength` UTF-16 code units, the most a spreadsheet cell keeps.
54
+ */
55
+ export const ATTACHMENT_LIMITS = {
56
+ maxBytes: 16 * 1024 * 1024,
57
+ maxRows: 100_000,
58
+ defaultMaxRows: 10_000,
59
+ maxCellLength: 32_767,
60
+ } as const
61
+
62
+ /** The type each attachment format answers with. */
63
+ export const CSV_CONTENT_TYPE = 'text/csv; charset=utf-8'
64
+ export const XLSX_CONTENT_TYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
65
+
66
+ /** Export procedures are served here, one path per procedure, and each export's description at its own path. */
67
+ export { CONNECT_PREFIX } from '@fabrika/auth-core'
68
+
69
+ /**
70
+ * Every connector call has a deadline; it bounds the retries too. Queries are safe to retry. An answer and a
71
+ * description are read under a byte budget, so another application cannot make the consumer buffer without bound.
72
+ */
73
+ export const CONNECT_LIMITS = {
74
+ defaultTimeoutMs: 3000,
75
+ maxTimeoutMs: 10_000,
76
+ retries: 2,
77
+ maxAnswerBytes: 1024 * 1024,
78
+ maxDescriptionBytes: 256 * 1024,
79
+ } as const
80
+
81
+ /** `products` version 1 → `products.v1`: the description's endpoint id, and the prefix of each procedure's. */
82
+ export function exportBase(name: string, version: number): string {
83
+ return `${name}.v${version}`
84
+ }
85
+
86
+ /** A link mount serves its procedures under `<prefix>/api`; everything else under the prefix is its SPA. */
87
+ export const LINK_API_SEGMENT = 'api'
88
+
89
+ /** The path segment a webhook appends for the binding a delivery addresses. */
90
+ export const WEBHOOK_BINDING_PARAMETER = 'bindingId'
91
+
92
+ /** Generated webhook binding endpoints (`webhooks.<name>.bind`, `.unbind`) live under this prefix, for signed-in people. */
93
+ export const WEBHOOKS_PREFIX = '/api'
94
+
95
+ /** The input field of a binding endpoint that names the scope value the binding reaches. */
96
+ export const WEBHOOK_SCOPE_FIELD = 'scope'
97
+
98
+ /** A GitHub account a binding accepts: an App installation or a repository, by numeric id. */
99
+ export const WEBHOOK_ACCOUNT_PATTERN = '^(?:installation|repository):[1-9][0-9]{0,19}$'
100
+
101
+ /**
102
+ * The share SPA's bootstrap, served by the framework under the link prefix and loaded before any other script: it
103
+ * takes the key out of the URL fragment and keeps it in a closure behind `SHARE_KEY_GLOBAL`.
104
+ */
105
+ export const LINK_BOOTSTRAP_FILE = '__fabrika-share.js'
106
+ export const SHARE_KEY_GLOBAL = '__fabrikaShareKey'
107
+
108
+ /** What every `c.spa` mount serves under its prefix: the browser's Operations DSN and the release, as JSON. */
109
+ export const BROWSER_CONFIG_FILE = '__fabrika-config.json'
110
+
111
+ /** What every `c.spa` mount serves under its prefix to the person its access admitted: label, mailbox and actions. */
112
+ export const PAGE_CALLER_FILE = '__fabrika-me.json'
113
+
114
+ /**
115
+ * The value a generated endpoint access case sends in an input field that names a member the handler looks up
116
+ * (`members` of `endpoint-access.fixtures.ts`); the endpoint access kit replaces it with a real member of the directory.
117
+ */
118
+ export const GENERATED_MEMBER = 'fabrika:member'
119
+
120
+ /**
121
+ * The value a generated endpoint access case sends in an input field that names an invitation the handler cancels
122
+ * (`invitations` of `endpoint-access.fixtures.ts`); the endpoint access kit replaces it with one open in the target's value.
123
+ */
124
+ export const GENERATED_INVITATION = 'fabrika:invitation'
125
+
126
+ /**
127
+ * The value a generated endpoint access case sends in an input field that names a share link the handler revokes
128
+ * (`links` of `endpoint-access.fixtures.ts`); the endpoint access kit replaces it with one the case's caller issued.
129
+ */
130
+ export const GENERATED_LINK = 'fabrika:link'
131
+
132
+ /**
133
+ * The prefix of the value a generated endpoint access case sends in an input field that names a row lying in its object
134
+ * (`rows` of `endpoint-access.fixtures.ts`), followed by the row's table: `fabrika:row:note_comments`. The endpoint
135
+ * access kit seeds one such row in each scope's object and sends that scope's key.
136
+ */
137
+ export const GENERATED_ROW_PREFIX = 'fabrika:row:'
138
+
139
+ export const JSON_ADMISSION: BodyAdmission = { contentTypes: ['application/json'], maxBytes: null }
140
+
141
+ /** `/api` + `notes.list` → `/api/notes.list`; a root prefix does not double the `/`. */
142
+ export function joinPath(prefix: string, rest: string): string {
143
+ return prefix === '/' ? `/${rest}` : `${prefix}/${rest}`
144
+ }
145
+
146
+ /**
147
+ * The paths of the endpoints of collection `name` under `base` (`FILES_PREFIX`, or a link mount's API prefix): the
148
+ * upload takes `/:owner` after its path for a signed-in caller, and the download `/:fileId`.
149
+ */
150
+ export function fileCollectionPaths(
151
+ base: string,
152
+ name: string,
153
+ ): { readonly upload: string; readonly download: string; readonly list: string; readonly remove: string } {
154
+ const at = joinPath(base, `files.${name}`)
155
+ return { upload: `${at}.upload`, download: at, list: `${at}.list`, remove: `${at}.remove` }
156
+ }