@fabrika/app 0.5.5 → 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.
- package/package.json +9 -9
- package/skill/SKILL.md +29 -21
- package/skill/references/app-setup.md +54 -36
- package/skill/references/dev.md +5 -5
- package/skill/references/endpoint.md +152 -25
- package/skill/references/errors.md +26 -23
- package/skill/references/files.md +187 -137
- package/skill/references/jobs.md +82 -35
- package/skill/references/public-intake.md +387 -236
- package/skill/references/spa.md +5 -5
- package/skill/references/start.md +2 -1
- package/skill/references/table-and-migration.md +24 -2
- package/skill/references/tests.md +8 -5
- package/skill/references/webhooks-links-connectors.md +65 -12
- package/src/bun.ts +3 -2
- package/src/client/contract.ts +184 -74
- package/src/client/index.ts +2 -1
- package/src/client/transport.ts +7 -2
- package/src/cloudflare.ts +2 -0
- package/src/contract/artifact-schema.ts +110 -15
- package/src/contract/build.ts +1184 -613
- package/src/contract/compile.ts +44 -10
- package/src/contract/connect.ts +71 -5
- package/src/contract/context.ts +132 -23
- package/src/contract/declaration.ts +78 -63
- package/src/contract/defaults.ts +64 -58
- package/src/contract/define.ts +76 -124
- package/src/contract/file.ts +78 -0
- package/src/contract/group.ts +244 -37
- package/src/contract/index.ts +18 -0
- package/src/contract/kit.ts +206 -126
- package/src/contract/platform.ts +8 -9
- package/src/contract/redirect.ts +49 -0
- package/src/contract/schema-from-shape.ts +7 -0
- package/src/contract/schema-shape.ts +10 -0
- package/src/contract/snapshot-source.ts +5 -0
- package/src/contract/token.ts +89 -0
- package/src/contract/webhook-binding.ts +26 -0
- package/src/contract/webhook-scheme.ts +32 -0
- package/src/files/handle.ts +22 -0
- package/src/files/kinds.ts +454 -0
- package/src/files/scan.ts +15 -9
- package/src/files/streams.ts +111 -0
- package/src/honeypot.ts +7 -0
- package/src/implement/anonymous.ts +18 -42
- package/src/implement/boot.ts +177 -159
- package/src/implement/bounded.ts +23 -0
- package/src/implement/browser-config.ts +4 -4
- package/src/implement/capability.ts +78 -2
- package/src/implement/{form-admission.ts → client-address.ts} +3 -38
- package/src/implement/dispatch.ts +810 -409
- package/src/implement/egress.ts +84 -0
- package/src/implement/faults.ts +11 -1
- package/src/implement/index.ts +3 -2
- package/src/implement/invocation.ts +135 -35
- package/src/implement/params.ts +88 -0
- package/src/implement/principal.ts +7 -0
- package/src/implement/public.ts +142 -0
- package/src/implement/tokens.ts +219 -0
- package/src/implement/types.ts +24 -3
- package/src/implement/urls.ts +78 -0
- package/src/implement/verifiers.ts +299 -0
- package/src/implement/webhook.ts +136 -55
- package/src/implement/workload.ts +16 -14
- package/src/index.ts +3 -0
- package/src/jobs/mail.ts +98 -10
- package/src/jobs/runner.ts +103 -71
- package/src/jobs/scrub.ts +4 -1
- package/src/jobs/types.ts +19 -2
- package/src/react/hooks.ts +2 -2
- package/src/react/provider.tsx +2 -2
- package/src/testing/dev.ts +2 -2
- package/src/testing/endpoint-access.ts +167 -128
- package/src/testing/kit.ts +79 -24
- package/src/testing/policy-isolation.ts +55 -29
- package/src/testing/proxy.ts +6 -2
- package/src/testing/sample-file.ts +30 -0
- package/src/client/files.ts +0 -135
- package/src/contract/file-schemas.ts +0 -25
- package/src/files/collections.ts +0 -848
package/src/client/files.ts
DELETED
|
@@ -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
|
-
})
|