@fabrika/app 0.3.3 → 0.4.1
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/README.md +78 -90
- package/package.json +77 -6
- package/skill/SKILL.md +319 -0
- package/skill/references/app-setup.md +370 -0
- package/skill/references/dev.md +90 -0
- package/skill/references/endpoint.md +640 -0
- package/skill/references/errors.md +108 -0
- package/skill/references/files.md +335 -0
- package/skill/references/jobs.md +169 -0
- package/skill/references/public-intake.md +1495 -0
- package/skill/references/spa.md +746 -0
- package/skill/references/start.md +179 -0
- package/skill/references/table-and-migration.md +264 -0
- package/skill/references/tests.md +308 -0
- package/skill/references/webhooks-links-connectors.md +190 -0
- package/src/agent-skill.ts +77 -0
- package/src/build.ts +53 -0
- package/src/bun.ts +302 -3
- package/src/client/caller.ts +24 -0
- package/src/client/contract.ts +331 -0
- package/src/client/files.ts +135 -0
- package/src/client/index.ts +19 -0
- package/src/client/rpc.ts +47 -0
- package/src/client/share.ts +14 -0
- package/src/{client.ts → client/transport.ts} +58 -61
- package/src/cloudflare.ts +139 -3
- package/src/contract/artifact-schema.ts +317 -0
- package/src/contract/attachment.ts +74 -0
- package/src/contract/build.ts +1932 -0
- package/src/contract/compile.ts +143 -0
- package/src/contract/connect.ts +170 -0
- package/src/contract/context.ts +444 -0
- package/src/contract/declaration.ts +310 -0
- package/src/contract/defaults.ts +161 -0
- package/src/contract/define.ts +567 -0
- package/src/contract/file-schemas.ts +25 -0
- package/src/contract/group.ts +263 -0
- package/src/contract/handled.ts +50 -0
- package/src/contract/index.ts +34 -0
- package/src/contract/kit.ts +607 -0
- package/src/contract/platform.ts +113 -0
- package/src/contract/problems.ts +30 -0
- package/src/contract/schema-from-shape.ts +152 -0
- package/src/contract/schema-shape.ts +407 -0
- package/src/contract/snapshot-source.ts +168 -0
- package/src/contract/vocabulary.ts +50 -0
- package/src/disk-assets.ts +25 -0
- package/src/fetch.ts +10 -0
- package/src/files/archive.ts +292 -0
- package/src/files/clamscan.ts +253 -0
- package/src/files/collections.ts +669 -0
- package/src/files/handle.ts +77 -0
- package/src/files/scan.ts +308 -0
- package/src/files/sniff.ts +70 -0
- package/src/files/treatment.ts +48 -0
- package/src/from-row.ts +53 -0
- package/src/implement/anonymous.ts +102 -0
- package/src/implement/attachment.ts +143 -0
- package/src/implement/boot.ts +824 -0
- package/src/implement/bounded.ts +72 -0
- package/src/implement/browser-config.ts +54 -0
- package/src/implement/capability.ts +439 -0
- package/src/implement/challenge.ts +120 -0
- package/src/implement/connect.ts +386 -0
- package/src/implement/dispatch.ts +1095 -0
- package/src/implement/environment.ts +47 -0
- package/src/implement/failures.ts +116 -0
- package/src/implement/faults.ts +46 -0
- package/src/implement/form-admission.ts +120 -0
- package/src/implement/headers.ts +17 -0
- package/src/implement/implement.ts +81 -0
- package/src/implement/index.ts +60 -0
- package/src/implement/invocation.ts +346 -0
- package/src/implement/membership.ts +485 -0
- package/src/implement/principal.ts +99 -0
- package/src/implement/project.ts +76 -0
- package/src/implement/serve.ts +142 -0
- package/src/implement/types.ts +215 -0
- package/src/implement/webhook.ts +108 -0
- package/src/implement/workload.ts +160 -0
- package/src/implement/xlsx.ts +275 -0
- package/src/index.ts +12 -13
- package/src/jobs/report.ts +134 -0
- package/src/jobs/runner.ts +760 -0
- package/src/jobs/schedule.ts +129 -0
- package/src/jobs/types.ts +44 -0
- package/src/react/caller.tsx +60 -0
- package/src/react/errors.ts +82 -0
- package/src/react/hooks.ts +281 -0
- package/src/react/index.ts +34 -0
- package/src/react/provider.tsx +179 -0
- package/src/react/store.ts +215 -0
- package/src/react/testing.ts +144 -0
- package/src/react/types.ts +40 -0
- package/src/rpc/dispatcher.ts +4 -19
- package/src/service/define.ts +121 -0
- package/src/service/index.ts +21 -0
- package/src/service/serve.ts +99 -0
- package/src/testing/caller.ts +123 -0
- package/src/testing/challenge.ts +10 -0
- package/src/testing/dev.ts +99 -0
- package/src/testing/endpoint-access.ts +809 -0
- package/src/testing/index.ts +36 -0
- package/src/testing/isolation.ts +25 -0
- package/src/testing/kit.ts +585 -0
- package/src/testing/membership.ts +797 -0
- package/src/testing/policy-isolation.ts +827 -0
- package/src/testing/preload.ts +6 -0
- package/src/testing/proxy.ts +87 -0
- package/src/testing/runner.ts +61 -0
- package/src/testing/status.ts +19 -0
- package/src/undumpable.ts +42 -0
- package/src/app.ts +0 -76
- package/src/middleware.ts +0 -38
- package/src/router.ts +0 -149
- 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,161 @@
|
|
|
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
|
+
/** The row of an SPA mount's bare prefix (`GET /admin`), which redirects to the mount (`/admin/`). */
|
|
115
|
+
export function spaPrefixRowId(mount: string): string {
|
|
116
|
+
return `${mount}.prefix`
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The value a generated endpoint access case sends in an input field that names a member the handler looks up
|
|
121
|
+
* (`members` of `endpoint-access.fixtures.ts`); the endpoint access kit replaces it with a real member of the directory.
|
|
122
|
+
*/
|
|
123
|
+
export const GENERATED_MEMBER = 'fabrika:member'
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The value a generated endpoint access case sends in an input field that names an invitation the handler cancels
|
|
127
|
+
* (`invitations` of `endpoint-access.fixtures.ts`); the endpoint access kit replaces it with one open in the target's value.
|
|
128
|
+
*/
|
|
129
|
+
export const GENERATED_INVITATION = 'fabrika:invitation'
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The value a generated endpoint access case sends in an input field that names a share link the handler revokes
|
|
133
|
+
* (`links` of `endpoint-access.fixtures.ts`); the endpoint access kit replaces it with one the case's caller issued.
|
|
134
|
+
*/
|
|
135
|
+
export const GENERATED_LINK = 'fabrika:link'
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The prefix of the value a generated endpoint access case sends in an input field that names a row lying in its object
|
|
139
|
+
* (`rows` of `endpoint-access.fixtures.ts`), followed by the row's table: `fabrika:row:note_comments`. The endpoint
|
|
140
|
+
* access kit seeds one such row in each scope's object and sends that scope's key.
|
|
141
|
+
*/
|
|
142
|
+
export const GENERATED_ROW_PREFIX = 'fabrika:row:'
|
|
143
|
+
|
|
144
|
+
export const JSON_ADMISSION: BodyAdmission = { contentTypes: ['application/json'], maxBytes: null }
|
|
145
|
+
|
|
146
|
+
/** `/api` + `notes.list` → `/api/notes.list`; a root prefix does not double the `/`. */
|
|
147
|
+
export function joinPath(prefix: string, rest: string): string {
|
|
148
|
+
return prefix === '/' ? `/${rest}` : `${prefix}/${rest}`
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The paths of the endpoints of collection `name` under `base` (`FILES_PREFIX`, or a link mount's API prefix): the
|
|
153
|
+
* upload takes `/:owner` after its path for a signed-in caller, and the download `/:fileId`.
|
|
154
|
+
*/
|
|
155
|
+
export function fileCollectionPaths(
|
|
156
|
+
base: string,
|
|
157
|
+
name: string,
|
|
158
|
+
): { readonly upload: string; readonly download: string; readonly list: string; readonly remove: string } {
|
|
159
|
+
const at = joinPath(base, `files.${name}`)
|
|
160
|
+
return { upload: `${at}.upload`, download: at, list: `${at}.list`, remove: `${at}.remove` }
|
|
161
|
+
}
|