@fabrika/app 0.6.0 → 0.8.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 +10 -9
- package/skill/SKILL.md +29 -21
- package/skill/references/app-setup.md +58 -36
- package/skill/references/dev.md +11 -9
- package/skill/references/endpoint.md +154 -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 +450 -255
- package/skill/references/spa.md +6 -5
- package/skill/references/start.md +2 -1
- package/skill/references/table-and-migration.md +24 -2
- package/skill/references/tests.md +14 -8
- package/skill/references/webhooks-links-connectors.md +65 -12
- package/src/bun.ts +3 -2
- package/src/client/contract.ts +221 -79
- package/src/client/index.ts +3 -1
- package/src/client/transport.ts +7 -2
- package/src/cloudflare.ts +2 -0
- package/src/contract/artifact-schema.ts +168 -14
- package/src/contract/build.ts +1282 -619
- package/src/contract/compile.ts +44 -10
- package/src/contract/connect.ts +79 -5
- package/src/contract/context.ts +132 -23
- package/src/contract/declaration.ts +78 -63
- package/src/contract/defaults.ts +142 -60
- package/src/contract/define.ts +92 -121
- 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 +210 -157
- package/src/implement/bounded.ts +23 -0
- package/src/implement/browser-config.ts +6 -5
- package/src/implement/capability.ts +78 -2
- package/src/implement/challenge.ts +203 -47
- package/src/implement/{form-admission.ts → client-address.ts} +3 -38
- package/src/implement/dispatch.ts +831 -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 +167 -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/challenge.ts +35 -1
- package/src/testing/dev.ts +2 -2
- package/src/testing/endpoint-access.ts +168 -128
- package/src/testing/kit.ts +90 -25
- 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
|
@@ -6,13 +6,17 @@ import {
|
|
|
6
6
|
type ArrayShape,
|
|
7
7
|
type AttachmentShape,
|
|
8
8
|
BUILTIN_ROLE_KEYS,
|
|
9
|
+
CONTRACT_FORMAT,
|
|
9
10
|
type ContractArtifact,
|
|
10
11
|
CROSS_ORIGIN_KINDS,
|
|
11
12
|
displayNameProblem,
|
|
12
13
|
type EndpointRow,
|
|
14
|
+
FILE_RESPONSE_KINDS,
|
|
15
|
+
isOwnWorkloadName,
|
|
13
16
|
JOB_MAILERS,
|
|
14
17
|
LOCALES,
|
|
15
18
|
mailTemplateProblems,
|
|
19
|
+
MAX_CACHE_AGE_SECONDS,
|
|
16
20
|
type MessageKind,
|
|
17
21
|
type NumberShape,
|
|
18
22
|
type OutputShape,
|
|
@@ -22,8 +26,20 @@ import {
|
|
|
22
26
|
type TablePolicyArtifact,
|
|
23
27
|
type TupleShape,
|
|
24
28
|
} from '@fabrika/auth-core'
|
|
29
|
+
import { isPolicyOrigin, WORKER_BLOB_SOURCE } from '@fabrika/proxy-core/headers'
|
|
25
30
|
import { z } from 'zod/v4'
|
|
26
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
ALTCHA_LIMITS,
|
|
33
|
+
altchaSolvable,
|
|
34
|
+
CHALLENGE_ACTION,
|
|
35
|
+
CHALLENGE_PREFIX,
|
|
36
|
+
challengePath,
|
|
37
|
+
challengeRowId,
|
|
38
|
+
JOB_LIMITS,
|
|
39
|
+
PROCEDURE_LIMITS,
|
|
40
|
+
} from './defaults'
|
|
41
|
+
import { TOKEN_MAX_DAYS } from './token'
|
|
42
|
+
import { webhookSchemeSchema } from './webhook-scheme'
|
|
27
43
|
|
|
28
44
|
const name = z.string().min(1)
|
|
29
45
|
const nullableName = name.nullable()
|
|
@@ -31,7 +47,6 @@ const names = z.array(name)
|
|
|
31
47
|
const record = <T extends z.core.$ZodType>(value: T) => z.record(z.string().min(1), value)
|
|
32
48
|
/** A contract's actions are exact: IAM treats `*` as a pattern, which would widen every grant naming it. */
|
|
33
49
|
const action = name.refine((value) => !value.includes('*'), 'an action is exact; * is not an action')
|
|
34
|
-
const nullableAction = action.nullable()
|
|
35
50
|
const actions = z.array(action)
|
|
36
51
|
/** IAM's own role keys win over an application role of the same key at resolution. */
|
|
37
52
|
const roleKey = name.refine((key) => !BUILTIN_ROLE_KEYS.includes(key), 'a built-in IAM role key is not a contract role name')
|
|
@@ -72,6 +87,8 @@ export const schemaShapeSchema: z.ZodType<SchemaShape> = z.lazy(() =>
|
|
|
72
87
|
z.strictObject({ type: z.literal('optional'), inner: schemaShapeSchema }),
|
|
73
88
|
z.strictObject({ type: z.literal('nullable'), inner: schemaShapeSchema }),
|
|
74
89
|
z.strictObject({ type: z.literal('union'), options: z.array(schemaShapeSchema) }),
|
|
90
|
+
z.strictObject({ type: z.literal('token'), purpose: name }),
|
|
91
|
+
z.strictObject({ type: z.literal('file'), kind: z.string().min(1) }),
|
|
75
92
|
])
|
|
76
93
|
)
|
|
77
94
|
|
|
@@ -102,7 +119,9 @@ const attachmentShape: z.ZodType<AttachmentShape> = z.strictObject({
|
|
|
102
119
|
})
|
|
103
120
|
|
|
104
121
|
/** What a procedure answers: a schema shape, or an attachment's file. */
|
|
105
|
-
|
|
122
|
+
const redirectShape = z.strictObject({ type: z.literal('redirect'), origins: z.array(z.string().startsWith('https://')) })
|
|
123
|
+
|
|
124
|
+
export const outputShapeSchema: z.ZodType<OutputShape> = z.union([schemaShapeSchema, attachmentShape, redirectShape])
|
|
106
125
|
|
|
107
126
|
const caller = z.union([z.enum(['human', 'machine', 'link']), z.templateLiteral(['app:', z.string().min(1)])])
|
|
108
127
|
const rpcCaller = z.enum(['human', 'machine'])
|
|
@@ -115,9 +134,27 @@ const accessForm = z.discriminatedUnion('form', [
|
|
|
115
134
|
z.strictObject({ form: z.literal('app') }),
|
|
116
135
|
])
|
|
117
136
|
|
|
137
|
+
const capabilityForm = z.discriminatedUnion('form', [
|
|
138
|
+
z.strictObject({ form: z.literal('over'), dimension: name }),
|
|
139
|
+
z.strictObject({ form: z.literal('in'), dimension: name, from: name }),
|
|
140
|
+
z.strictObject({ form: z.literal('on'), dimension: name, from: name }),
|
|
141
|
+
])
|
|
142
|
+
|
|
118
143
|
const requirement = z.discriminatedUnion('kind', [
|
|
119
144
|
z.strictObject({ kind: z.literal('member') }),
|
|
120
145
|
z.strictObject({ kind: z.literal('action'), action, access: accessForm }),
|
|
146
|
+
z.strictObject({
|
|
147
|
+
kind: z.literal('capabilities'),
|
|
148
|
+
capabilities: record(z.strictObject({ action, access: capabilityForm })).refine(
|
|
149
|
+
(capabilities) => Object.keys(capabilities).length >= 2,
|
|
150
|
+
'several capabilities are at least two',
|
|
151
|
+
),
|
|
152
|
+
}),
|
|
153
|
+
])
|
|
154
|
+
|
|
155
|
+
const endpointCacheSchema = z.union([
|
|
156
|
+
z.literal('immutable'),
|
|
157
|
+
z.strictObject({ maxAgeSeconds: z.int().min(1).max(MAX_CACHE_AGE_SECONDS) }),
|
|
121
158
|
])
|
|
122
159
|
|
|
123
160
|
const rowBase = {
|
|
@@ -128,6 +165,7 @@ const rowBase = {
|
|
|
128
165
|
admission: z.strictObject({ contentTypes: z.tuple([name], name), maxBytes: z.int().positive().nullable() }).nullable(),
|
|
129
166
|
input: schemaShapeSchema.nullable(),
|
|
130
167
|
output: outputShapeSchema.nullable(),
|
|
168
|
+
response: z.literal('file').optional(),
|
|
131
169
|
}
|
|
132
170
|
|
|
133
171
|
const endpointRow: z.ZodType<EndpointRow> = z.union([
|
|
@@ -144,6 +182,31 @@ const endpointRow: z.ZodType<EndpointRow> = z.union([
|
|
|
144
182
|
callers: z.tuple([]),
|
|
145
183
|
requirement: z.null(),
|
|
146
184
|
crossOrigin: z.enum(['environment', 'any']).optional(),
|
|
185
|
+
authorization: z.literal('forward').optional(),
|
|
186
|
+
}).refine((row) => row.authorization === undefined || row.kind === 'webhook', "only a webhook forwards the request's Authorization"),
|
|
187
|
+
// A public procedure is a mount's procedure anyone calls: it admits a body and answers its output.
|
|
188
|
+
z.strictObject({
|
|
189
|
+
...rowBase,
|
|
190
|
+
kind: z.literal('public'),
|
|
191
|
+
source: z.strictObject({ kind: z.literal('mount'), name }),
|
|
192
|
+
output: outputShapeSchema,
|
|
193
|
+
callers: z.tuple([]),
|
|
194
|
+
requirement: z.null(),
|
|
195
|
+
crossOrigin: z.enum(['environment', 'any']).optional(),
|
|
196
|
+
cache: endpointCacheSchema.optional(),
|
|
197
|
+
}).refine((row) => row.cache === undefined || row.method === 'GET', 'only a GET answer is cached'),
|
|
198
|
+
// The framework's own GET that issues the ALTCHA challenge of the public procedure its source names: no body, no schemas.
|
|
199
|
+
z.strictObject({
|
|
200
|
+
...rowBase,
|
|
201
|
+
kind: z.literal('public'),
|
|
202
|
+
source: z.strictObject({ kind: z.literal('challenge'), name }),
|
|
203
|
+
method: z.literal('GET'),
|
|
204
|
+
admission: z.null(),
|
|
205
|
+
input: z.null(),
|
|
206
|
+
output: z.null(),
|
|
207
|
+
callers: z.tuple([]),
|
|
208
|
+
requirement: z.null(),
|
|
209
|
+
crossOrigin: z.enum(['environment', 'any']).optional(),
|
|
147
210
|
}),
|
|
148
211
|
// A public SPA is a fixed static row: GET only, no body, no schemas.
|
|
149
212
|
z.strictObject({
|
|
@@ -157,7 +220,11 @@ const endpointRow: z.ZodType<EndpointRow> = z.union([
|
|
|
157
220
|
requirement: z.null(),
|
|
158
221
|
}),
|
|
159
222
|
]).refine(
|
|
160
|
-
// Only
|
|
223
|
+
// Only a procedure answers a stored file, and exactly when its output is one.
|
|
224
|
+
(row) => (row.response === 'file') === (row.output?.type === 'file') && (row.response === undefined || FILE_RESPONSE_KINDS.includes(row.kind)),
|
|
225
|
+
'only a procedure whose output is a file answers a stored file',
|
|
226
|
+
).refine(
|
|
227
|
+
// Only an RPC procedure, a public procedure and a form open to another origin, and only the anonymous two to any (ADR-0022).
|
|
161
228
|
(row) =>
|
|
162
229
|
!('crossOrigin' in row) || row.crossOrigin === undefined
|
|
163
230
|
|| (CROSS_ORIGIN_KINDS.includes(row.kind) && (row.crossOrigin !== 'any' || ANY_ORIGIN_KINDS.includes(row.kind))),
|
|
@@ -169,11 +236,25 @@ const scopeBinding = z.discriminatedUnion('kind', [
|
|
|
169
236
|
z.strictObject({ kind: z.literal('dimension'), dimension: name }),
|
|
170
237
|
])
|
|
171
238
|
|
|
172
|
-
|
|
239
|
+
/** A form's rate, per minute, as the releases that declared forms (up to 0.5) bounded it. */
|
|
240
|
+
const formRate = z.int().positive().max(10_000)
|
|
173
241
|
const applicationVariable = z.string().regex(/^[A-Z][A-Z0-9_]*$/)
|
|
242
|
+
const bucket = z.int().positive().max(PROCEDURE_LIMITS.maxPerMinute).nullable()
|
|
243
|
+
const challenge = z.strictObject({ provider: z.literal('turnstile'), secret: applicationVariable, siteKey: applicationVariable.optional() })
|
|
244
|
+
const challengeActionSchema = z.string().regex(CHALLENGE_ACTION)
|
|
245
|
+
const procedureChallenge = z.discriminatedUnion('provider', [
|
|
246
|
+
challenge.extend({ action: challengeActionSchema }),
|
|
247
|
+
z.strictObject({
|
|
248
|
+
provider: z.literal('altcha'),
|
|
249
|
+
secret: applicationVariable,
|
|
250
|
+
action: challengeActionSchema,
|
|
251
|
+
cost: z.int().min(ALTCHA_LIMITS.minCost).max(ALTCHA_LIMITS.maxCost),
|
|
252
|
+
maxAgeMs: z.int().min(ALTCHA_LIMITS.minMaxAgeMs).max(ALTCHA_LIMITS.maxMaxAgeMs),
|
|
253
|
+
}).refine((challenge) => altchaSolvable(challenge.cost, challenge.maxAgeMs), 'a slow phone does not solve this challenge in time'),
|
|
254
|
+
])
|
|
174
255
|
|
|
175
256
|
const mount = z.discriminatedUnion('kind', [
|
|
176
|
-
z.strictObject({ kind: z.literal('rpc'), prefix: name, callers: z.
|
|
257
|
+
z.strictObject({ kind: z.literal('rpc'), prefix: name, callers: z.array(rpcCaller), compatibility: z.literal('backward').optional() }),
|
|
177
258
|
z.strictObject({
|
|
178
259
|
kind: z.literal('link'),
|
|
179
260
|
prefix: name,
|
|
@@ -191,15 +272,28 @@ const mount = z.discriminatedUnion('kind', [
|
|
|
191
272
|
appendTo: name,
|
|
192
273
|
rate: z.strictObject({ perClient: formRate.optional(), perForm: formRate.optional(), bytesPerHour: z.int().positive().optional() }).optional(),
|
|
193
274
|
honeypot: name.optional(),
|
|
194
|
-
challenge:
|
|
275
|
+
challenge: challenge.optional(),
|
|
195
276
|
then: name.optional(),
|
|
196
277
|
files: z.record(name, z.strictObject({ collection: name, required: z.boolean() })).optional(),
|
|
197
278
|
}),
|
|
198
|
-
z.strictObject({
|
|
279
|
+
z.strictObject({
|
|
280
|
+
kind: z.literal('webhook'),
|
|
281
|
+
path: name,
|
|
282
|
+
scheme: webhookSchemeSchema,
|
|
283
|
+
boundTo: scopeBinding,
|
|
284
|
+
actions,
|
|
285
|
+
timeoutMs: z.int().positive().max(PROCEDURE_LIMITS.maxTimeoutMs),
|
|
286
|
+
}),
|
|
199
287
|
])
|
|
200
288
|
|
|
201
289
|
const cspSource = z.strictObject({ origin: name, purpose: name })
|
|
202
290
|
|
|
291
|
+
/** One action as its text (every release's form), or several distinct ones as a list of at least two. */
|
|
292
|
+
const tableAction = z.union([
|
|
293
|
+
action,
|
|
294
|
+
z.array(action).min(2).refine((list) => new Set(list).size === list.length, 'a table operation names each action once'),
|
|
295
|
+
])
|
|
296
|
+
|
|
203
297
|
const tablePolicy: z.ZodType<TablePolicyArtifact> = z.strictObject({
|
|
204
298
|
scope: z.discriminatedUnion('mode', [
|
|
205
299
|
z.strictObject({
|
|
@@ -214,10 +308,10 @@ const tablePolicy: z.ZodType<TablePolicyArtifact> = z.strictObject({
|
|
|
214
308
|
z.strictObject({ mode: z.literal('app'), reason: name, key: name }),
|
|
215
309
|
]),
|
|
216
310
|
actions: z.strictObject({
|
|
217
|
-
read:
|
|
218
|
-
insert:
|
|
219
|
-
update:
|
|
220
|
-
delete:
|
|
311
|
+
read: tableAction,
|
|
312
|
+
insert: tableAction.nullable(),
|
|
313
|
+
update: tableAction.nullable(),
|
|
314
|
+
delete: tableAction.nullable(),
|
|
221
315
|
appRead: z.array(action).nonempty().optional(),
|
|
222
316
|
appCount: z.array(action).nonempty().optional(),
|
|
223
317
|
}),
|
|
@@ -226,9 +320,11 @@ const tablePolicy: z.ZodType<TablePolicyArtifact> = z.strictObject({
|
|
|
226
320
|
version: nullableName,
|
|
227
321
|
createdBy: nullableName,
|
|
228
322
|
columns: record(z.strictObject({ read: action.optional(), write: action.optional() })),
|
|
323
|
+
files: z.record(name, name).optional(),
|
|
229
324
|
})
|
|
230
325
|
|
|
231
326
|
export const contractArtifactSchema: z.ZodType<ContractArtifact> = z.strictObject({
|
|
327
|
+
format: z.literal(CONTRACT_FORMAT),
|
|
232
328
|
app: name,
|
|
233
329
|
dimensions: record(z.strictObject({
|
|
234
330
|
in: nullableName,
|
|
@@ -260,11 +356,12 @@ export const contractArtifactSchema: z.ZodType<ContractArtifact> = z.strictObjec
|
|
|
260
356
|
link: z.union([z.boolean(), z.array(name).nonempty()]),
|
|
261
357
|
scan: z.literal(true).optional(),
|
|
262
358
|
})),
|
|
359
|
+
fileKinds: z.record(name, z.strictObject({ types: names, maxBytes: z.int().positive(), scan: z.literal(true).optional() })).optional(),
|
|
263
360
|
jobs: record(z.strictObject({
|
|
264
361
|
schedule: name,
|
|
265
362
|
actions,
|
|
266
363
|
scope: scopeBinding,
|
|
267
|
-
|
|
364
|
+
timeoutMs: z.int().positive().max(JOB_LIMITS.maxTimeoutMs),
|
|
268
365
|
retries: z.int().nonnegative(),
|
|
269
366
|
})),
|
|
270
367
|
exports: record(z.strictObject({ version: z.int().positive(), consumers: names, audience: name })),
|
|
@@ -285,6 +382,7 @@ export const contractArtifactSchema: z.ZodType<ContractArtifact> = z.strictObjec
|
|
|
285
382
|
frameSrc: z.array(cspSource).nonempty().optional(),
|
|
286
383
|
styleSrc: z.array(cspSource).nonempty().optional(),
|
|
287
384
|
fontSrc: z.array(cspSource).nonempty().optional(),
|
|
385
|
+
workerSrc: z.tuple([z.strictObject({ origin: z.literal(WORKER_BLOB_SOURCE), purpose: z.string().min(1) })]).optional(),
|
|
288
386
|
}),
|
|
289
387
|
tables: record(tablePolicy),
|
|
290
388
|
endpoints: z.array(endpointRow).superRefine((rows, context) => {
|
|
@@ -308,7 +406,18 @@ export const contractArtifactSchema: z.ZodType<ContractArtifact> = z.strictObjec
|
|
|
308
406
|
issues: nullableName,
|
|
309
407
|
then: name.optional(),
|
|
310
408
|
mailer: z.enum(JOB_MAILERS).optional(),
|
|
311
|
-
|
|
409
|
+
actions: actions.nonempty().optional(),
|
|
410
|
+
kicks: z.array(name).nonempty().optional(),
|
|
411
|
+
binds: name.optional(),
|
|
412
|
+
signs: z.array(name).nonempty().optional(),
|
|
413
|
+
egress: z.array(z.string().refine((origin) => isPolicyOrigin(origin))).nonempty().optional(),
|
|
414
|
+
})).superRefine((declared, context) => {
|
|
415
|
+
for (const [site, reach] of Object.entries(declared)) {
|
|
416
|
+
if (reach.actions !== undefined && !isOwnWorkloadName('public', site)) {
|
|
417
|
+
context.addIssue({ code: 'custom', path: [site, 'actions'], message: 'workload actions belong to a public procedure, keyed by its endpoint id' })
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}).optional(),
|
|
312
421
|
directories: record(z.strictObject({ dimension: name, lists: z.boolean() })).optional(),
|
|
313
422
|
locale: z.enum(LOCALES).optional(),
|
|
314
423
|
displayName: z.string().refine((value) => displayNameProblem(value) === null, 'not a display name').optional(),
|
|
@@ -316,6 +425,51 @@ export const contractArtifactSchema: z.ZodType<ContractArtifact> = z.strictObjec
|
|
|
316
425
|
.optional(),
|
|
317
426
|
members: z.strictObject({ manage: action, list: action }).optional(),
|
|
318
427
|
directory: z.literal('labels').optional(),
|
|
428
|
+
procedures: record(z.strictObject({
|
|
429
|
+
rate: z.strictObject({
|
|
430
|
+
perClient: bucket,
|
|
431
|
+
perCaller: bucket,
|
|
432
|
+
perEndpoint: bucket,
|
|
433
|
+
bytesPerHour: z.int().positive().max(PROCEDURE_LIMITS.maxBytesPerHour).nullable(),
|
|
434
|
+
}),
|
|
435
|
+
challenge: procedureChallenge.nullable(),
|
|
436
|
+
timeoutMs: z.int().positive().max(PROCEDURE_LIMITS.maxTimeoutMs),
|
|
437
|
+
query: schemaShapeSchema.optional(),
|
|
438
|
+
changesStateOnGet: z.literal(true).optional(),
|
|
439
|
+
})).optional(),
|
|
440
|
+
tokens: record(z.strictObject({ claims: schemaShapeSchema, maxDays: z.int().positive().max(TOKEN_MAX_DAYS) })).optional(),
|
|
441
|
+
}).superRefine((artifact, context) => {
|
|
442
|
+
for (const row of artifact.endpoints) {
|
|
443
|
+
if (row.kind === 'public' && row.source.kind !== 'challenge' && artifact.declared?.[row.id]?.actions === undefined) {
|
|
444
|
+
context.addIssue({ code: 'custom', path: ['declared', row.id], message: 'a public procedure declares its workload actions' })
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
// Every ALTCHA challenge has exactly its own issuer, under its action, from the procedure's origins, with its own buckets.
|
|
448
|
+
const rows = new Map(artifact.endpoints.map((row) => [row.id, row]))
|
|
449
|
+
const issuers = artifact.endpoints.filter((row) => row.source.kind === 'challenge')
|
|
450
|
+
for (const [id, rules] of Object.entries(artifact.procedures ?? {})) {
|
|
451
|
+
if (rules.challenge?.provider !== 'altcha') continue
|
|
452
|
+
const issuer = rows.get(challengeRowId(id))
|
|
453
|
+
const procedure = rows.get(id)
|
|
454
|
+
if (
|
|
455
|
+
issuer === undefined || procedure === undefined || issuer.source.kind !== 'challenge' || issuer.source.name !== id
|
|
456
|
+
|| issuer.path !== challengePath(rules.challenge.action) || issuer.crossOrigin !== procedure.crossOrigin
|
|
457
|
+
|| artifact.procedures?.[issuer.id]?.challenge !== null
|
|
458
|
+
) {
|
|
459
|
+
context.addIssue({ code: 'custom', path: ['procedures', id, 'challenge'], message: 'an ALTCHA challenge has its own issuer row' })
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
for (const issuer of issuers) {
|
|
463
|
+
if (issuer.id !== challengeRowId(issuer.source.name) || artifact.procedures?.[issuer.source.name]?.challenge?.provider !== 'altcha') {
|
|
464
|
+
context.addIssue({ code: 'custom', path: ['endpoints'], message: `'${issuer.id}' issues the challenge of no ALTCHA procedure` })
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
if (issuers.length > 0 && !artifact.reservedPrefixes.includes(CHALLENGE_PREFIX)) {
|
|
468
|
+
context.addIssue({ code: 'custom', path: ['reservedPrefixes'], message: `an issuer row reserves ${CHALLENGE_PREFIX}` })
|
|
469
|
+
}
|
|
470
|
+
if ((artifact.csp.workerSrc !== undefined) !== issuers.length > 0) {
|
|
471
|
+
context.addIssue({ code: 'custom', path: ['csp', 'workerSrc'], message: 'workerSrc admits the workers of an ALTCHA challenge, and only those' })
|
|
472
|
+
}
|
|
319
473
|
})
|
|
320
474
|
|
|
321
475
|
function mailLocales(kind: MessageKind) {
|