@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
@@ -1,135 +0,0 @@
1
- // The file collections of a typed client: `api.files.<name>` for a signed-in caller of an RPC mount, and
2
- // `<link client>.files.<name>` for a link holder, whose owner is the link's object. The paths come from
3
- // `fileCollectionPaths`, the code that gives the artifact its rows; every answer is parsed before the caller sees it.
4
-
5
- import { z } from 'zod/v4'
6
- import { fileCollectionPaths, FILES_PREFIX, joinPath, LINK_API_SEGMENT } from '../contract/defaults'
7
- import type { FileCollection } from '../contract/define'
8
- import { FileListOutput } from '../contract/file-schemas'
9
- import { type BounceConfig, exchange, exchangeFile, RpcError, type RpcFetch } from './transport'
10
-
11
- /** A page of an owner's files, oldest first, as `files.<name>.list` answers it. */
12
- export type FileList = z.output<typeof FileListOutput>
13
-
14
- export interface FilePage {
15
- /** The id of the last file of the previous page. */
16
- readonly after?: string
17
- /** 1–100, 50 by default. */
18
- readonly limit?: number
19
- }
20
-
21
- export interface UploadOptions {
22
- /** The file's name, metadata only; a `File`'s own name by default. */
23
- readonly name?: string
24
- }
25
-
26
- /** One collection, for a signed-in caller: the owner is a parameter, checked against the caller's grants. */
27
- export interface FileClient {
28
- /** Uploads `file` (its `type` is the declared media type) to `owner`; answers the new file's id. */
29
- upload(owner: string, file: Blob, options?: UploadOptions): Promise<{ readonly id: string }>
30
- /** The owner's files, oldest first. */
31
- list(input: { readonly owner: string } & FilePage): Promise<FileList>
32
- /** The file `id`, named as it was uploaded. A refusal is an `RpcError`: 409 `quarantined` while it is being scanned. */
33
- download(id: string): Promise<File>
34
- remove(input: { readonly id: string }): Promise<{ readonly id: string }>
35
- }
36
-
37
- /** One collection, for a link holder: the owner is the object the key is bound to, never a parameter. */
38
- export interface LinkFileClient {
39
- upload(file: Blob, options?: UploadOptions): Promise<{ readonly id: string }>
40
- list(input?: FilePage): Promise<FileList>
41
- download(id: string): Promise<File>
42
- }
43
-
44
- /** The collections the holders of link mount `M` reach: those with `link: true`, and those whose `link` names `M`. */
45
- export type LinkCollections<F, M> = {
46
- [N in keyof F]: F[N] extends FileCollection<string, infer L>
47
- ? [L] extends [true] ? N : [L] extends [readonly (infer Named)[]] ? M extends Named ? N : never : never
48
- : never
49
- }[keyof F]
50
-
51
- /** What a client sends with every request: its key as a bearer, or the page's session. */
52
- export interface FileTransport {
53
- readonly origin: string
54
- readonly fetch: RpcFetch
55
- readonly key: string | undefined
56
- readonly bounce: BounceConfig | null
57
- }
58
-
59
- const Uploaded = z.strictObject({ id: z.uuid() })
60
-
61
- /** The request, built inside the transport's guard: the key as a bearer and no cookie, or the page's session. */
62
- function init(transport: FileTransport, method: 'GET' | 'POST', contentType: string | null, body?: BodyInit): () => RequestInit {
63
- return () => {
64
- const headers = new Headers()
65
- if (contentType !== null) {
66
- headers.set('Content-Type', contentType)
67
- }
68
- if (transport.key !== undefined) {
69
- headers.set('Authorization', `Bearer ${transport.key}`)
70
- }
71
- return { method, credentials: transport.key === undefined ? 'same-origin' : 'omit', headers, ...(body === undefined ? {} : { body }) }
72
- }
73
- }
74
-
75
- async function call<S extends z.ZodType>(transport: FileTransport, url: string, input: unknown, output: S, id: string): Promise<z.output<S>> {
76
- const request = init(transport, 'POST', 'application/json', JSON.stringify(input ?? {}))
77
- const parsed = z.safeParse(output, await exchange(transport.fetch, url, request, transport.bounce))
78
- if (!parsed.success) {
79
- throw new RpcError({ type: 'invalid-response', message: `'${id}' answered a value its output schema refuses`, httpStatus: 200 })
80
- }
81
- return parsed.data
82
- }
83
-
84
- async function upload(transport: FileTransport, path: string, file: Blob, options: UploadOptions, id: string): Promise<{ readonly id: string }> {
85
- const name = options.name ?? (file instanceof File ? file.name : '')
86
- const url = `${transport.origin}${path}${name === '' ? '' : `?${new URLSearchParams({ name })}`}`
87
- const parsed = z.safeParse(Uploaded, await exchange(transport.fetch, url, init(transport, 'POST', file.type, file), transport.bounce))
88
- if (!parsed.success) {
89
- throw new RpcError({ type: 'invalid-response', message: `'${id}' answered a value its output schema refuses`, httpStatus: 200 })
90
- }
91
- return parsed.data
92
- }
93
-
94
- /** `filename*=UTF-8''…` of a download's `Content-Disposition`, as `files/treatment.ts` writes it. */
95
- function nameOf(disposition: string | null, fallback: string): string {
96
- const encoded = disposition?.match(/filename\*=UTF-8''([^;]+)/)?.[1]
97
- if (encoded === undefined) {
98
- return fallback
99
- }
100
- try {
101
- return decodeURIComponent(encoded)
102
- } catch {
103
- return fallback
104
- }
105
- }
106
-
107
- async function download(transport: FileTransport, path: string, file: string): Promise<File> {
108
- const url = `${transport.origin}${path}/${encodeURIComponent(file)}`
109
- const answer = await exchangeFile(transport.fetch, url, init(transport, 'GET', null), transport.bounce)
110
- return new File([answer.body], nameOf(answer.disposition, file), { type: answer.body.type })
111
- }
112
-
113
- /** The collection `name` for a signed-in caller. */
114
- export function sessionFiles(transport: FileTransport, name: string): FileClient {
115
- const paths = fileCollectionPaths(FILES_PREFIX, name)
116
- const id = `files.${name}`
117
- return Object.freeze({
118
- upload: (owner: string, file: Blob, options: UploadOptions = {}) =>
119
- upload(transport, `${paths.upload}/${encodeURIComponent(owner)}`, file, options, `${id}.upload`),
120
- list: (input: { readonly owner: string } & FilePage) => call(transport, `${transport.origin}${paths.list}`, input, FileListOutput, `${id}.list`),
121
- download: (file: string) => download(transport, paths.download, file),
122
- remove: (input: { readonly id: string }) => call(transport, `${transport.origin}${paths.remove}`, input, Uploaded, `${id}.remove`),
123
- })
124
- }
125
-
126
- /** The collection `name` for a holder of a key of the link mount at `prefix`. */
127
- export function linkFiles(transport: FileTransport, prefix: string, mount: string, name: string): LinkFileClient {
128
- const paths = fileCollectionPaths(joinPath(prefix, LINK_API_SEGMENT), name)
129
- const id = `${mount}.files.${name}`
130
- return Object.freeze({
131
- upload: (file: Blob, options: UploadOptions = {}) => upload(transport, paths.upload, file, options, `${id}.upload`),
132
- list: (input: FilePage = {}) => call(transport, `${transport.origin}${paths.list}`, input, FileListOutput, `${id}.list`),
133
- download: (file: string) => download(transport, paths.download, file),
134
- })
135
- }
@@ -1,25 +0,0 @@
1
- // The wire schemas of a collection's list endpoint, shared by the files runtime and the browser client. Browser-safe:
2
- // zod and the contract's limits only. `FILE_LIST_INPUT` and `FILE_LIST_OUTPUT` of the contract describe the same.
3
-
4
- import { z } from 'zod/v4'
5
- import { FILE_LIMITS } from './defaults'
6
-
7
- /** `files.<name>.list`'s input. */
8
- export const FileListInput = z.strictObject({
9
- owner: z.string().min(1),
10
- after: z.uuid().optional(),
11
- limit: z.number().int().min(1).max(FILE_LIMITS.listPageMax).optional(),
12
- })
13
- /** A link holder's `<mount>.files.<name>.list`: no owner, which is the link's object; `FILE_LINK_LIST_INPUT` describes it. */
14
- export const FileLinkListInput = FileListInput.omit({ owner: true })
15
- export const FileListOutput = z.strictObject({
16
- items: z.array(z.strictObject({
17
- id: z.uuid(),
18
- name: z.string().min(1).nullable(),
19
- type: z.string().min(1),
20
- size: z.number().int().min(0),
21
- status: z.enum(['live', 'quarantined', 'rejected']),
22
- createdAt: z.iso.datetime(),
23
- })).max(FILE_LIMITS.listPageMax),
24
- next: z.uuid().nullable(),
25
- })