@fabrika/app 0.8.2 → 0.9.0-cloudflare.2

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 (201) hide show
  1. package/package.json +40 -24
  2. package/skill/SKILL.md +29 -3
  3. package/skill/references/app-setup.md +29 -11
  4. package/skill/references/dev.md +2 -1
  5. package/skill/references/endpoint.md +12 -3
  6. package/skill/references/errors.md +39 -35
  7. package/skill/references/files.md +4 -2
  8. package/skill/references/jobs.md +9 -0
  9. package/skill/references/table-and-migration.md +19 -2
  10. package/skill/references/tests.md +33 -1
  11. package/skill/references/webhooks-links-connectors.md +1 -1
  12. package/src/build-recipe.ts +198 -0
  13. package/src/build.ts +60 -4
  14. package/src/bun.ts +33 -8
  15. package/src/cloudflare/application.ts +352 -0
  16. package/src/cloudflare/bindings.ts +159 -0
  17. package/src/cloudflare/names.ts +16 -0
  18. package/src/cloudflare/ops.ts +246 -0
  19. package/src/cloudflare/r2-files.ts +172 -0
  20. package/src/cloudflare.ts +99 -146
  21. package/src/contract/app-id.ts +5 -0
  22. package/src/contract/artifact-schema.ts +8 -1
  23. package/src/contract/build.ts +68 -11
  24. package/src/contract/compile.ts +1 -0
  25. package/src/contract/context.ts +9 -0
  26. package/src/contract/declaration.ts +2 -0
  27. package/src/contract/define.ts +28 -2
  28. package/src/contract/group.ts +10 -1
  29. package/src/contract/kit.ts +6 -2
  30. package/src/contract/platform.ts +3 -2
  31. package/src/contract/vocabulary.ts +26 -8
  32. package/src/dev/changes.ts +35 -0
  33. package/src/dev/contract-digest.ts +17 -0
  34. package/src/dev/index.ts +11 -0
  35. package/src/dev/launcher.ts +713 -0
  36. package/src/dev/output.ts +72 -0
  37. package/src/dev/wire.ts +136 -0
  38. package/src/disk-assets.ts +8 -3
  39. package/src/files/archive.ts +2 -2
  40. package/src/files/job-names.ts +12 -0
  41. package/src/files/kinds.ts +19 -7
  42. package/src/files/scan.ts +3 -4
  43. package/src/files/streams.ts +3 -1
  44. package/src/implement/anonymous.ts +11 -1
  45. package/src/implement/boot.ts +70 -7
  46. package/src/implement/bounded.ts +21 -2
  47. package/src/implement/capability.ts +18 -3
  48. package/src/implement/dispatch.ts +251 -147
  49. package/src/implement/egress.ts +341 -8
  50. package/src/implement/failures.ts +17 -1
  51. package/src/implement/faults.ts +1 -0
  52. package/src/implement/index.ts +1 -0
  53. package/src/implement/invocation.ts +18 -9
  54. package/src/implement/principal.ts +25 -2
  55. package/src/implement/terminals.ts +93 -0
  56. package/src/implement/workload.ts +24 -13
  57. package/src/jobs/report.ts +5 -1
  58. package/src/jobs/runner.ts +40 -19
  59. package/src/jobs/schedule.ts +7 -1
  60. package/src/jobs/scrub.ts +14 -1
  61. package/src/spa-fallback.ts +12 -0
  62. package/src/testing/endpoint-access.ts +307 -152
  63. package/src/testing/isolation.ts +8 -1
  64. package/src/testing/kit.ts +20 -2
  65. package/src/testing/proxy.ts +5 -5
  66. package/types/src/agent-skill.d.ts +29 -0
  67. package/types/src/build-recipe.d.ts +70 -0
  68. package/types/src/build.d.ts +29 -0
  69. package/types/src/bun.d.ts +66 -0
  70. package/types/src/client/caller.d.ts +19 -0
  71. package/types/src/client/contract.d.ts +131 -0
  72. package/types/src/client/error-reporting.d.ts +66 -0
  73. package/types/src/client/index.d.ts +4 -0
  74. package/types/src/client/rpc.d.ts +14 -0
  75. package/types/src/client/share.d.ts +2 -0
  76. package/types/src/client/transport.d.ts +42 -0
  77. package/types/src/cloudflare/application.d.ts +42 -0
  78. package/types/src/cloudflare/bindings.d.ts +33 -0
  79. package/types/src/cloudflare/names.d.ts +11 -0
  80. package/types/src/cloudflare/ops.d.ts +46 -0
  81. package/types/src/cloudflare/r2-files.d.ts +9 -0
  82. package/types/src/cloudflare.d.ts +42 -0
  83. package/types/src/contract/app-id.d.ts +2 -0
  84. package/types/src/contract/artifact-schema.d.ts +8 -0
  85. package/types/src/contract/attachment.d.ts +41 -0
  86. package/types/src/contract/build.d.ts +12 -0
  87. package/types/src/contract/compile.d.ts +11 -0
  88. package/types/src/contract/connect.d.ts +51 -0
  89. package/types/src/contract/context.d.ts +510 -0
  90. package/types/src/contract/declaration.d.ts +307 -0
  91. package/types/src/contract/defaults.d.ts +208 -0
  92. package/types/src/contract/define.d.ts +376 -0
  93. package/types/src/contract/file.d.ts +44 -0
  94. package/types/src/contract/group.d.ts +282 -0
  95. package/types/src/contract/handled.d.ts +18 -0
  96. package/types/src/contract/index.d.ts +19 -0
  97. package/types/src/contract/kit.d.ts +211 -0
  98. package/types/src/contract/platform.d.ts +9 -0
  99. package/types/src/contract/problems.d.ts +14 -0
  100. package/types/src/contract/redirect.d.ts +25 -0
  101. package/types/src/contract/schema-from-shape.d.ts +10 -0
  102. package/types/src/contract/schema-shape.d.ts +5 -0
  103. package/types/src/contract/snapshot-source.d.ts +15 -0
  104. package/types/src/contract/token.d.ts +58 -0
  105. package/types/src/contract/vocabulary.d.ts +32 -0
  106. package/types/src/contract/webhook-binding.d.ts +24 -0
  107. package/types/src/contract/webhook-scheme.d.ts +23 -0
  108. package/types/src/dev/changes.d.ts +7 -0
  109. package/types/src/dev/contract-digest.d.ts +1 -0
  110. package/types/src/dev/index.d.ts +2 -0
  111. package/types/src/dev/launcher.d.ts +32 -0
  112. package/types/src/dev/output.d.ts +28 -0
  113. package/types/src/dev/wire.d.ts +126 -0
  114. package/types/src/disk-assets.d.ts +6 -0
  115. package/types/src/errors.d.ts +46 -0
  116. package/types/src/fetch.d.ts +7 -0
  117. package/types/src/files/archive.d.ts +26 -0
  118. package/types/src/files/clamscan.d.ts +32 -0
  119. package/types/src/files/handle.d.ts +106 -0
  120. package/types/src/files/job-names.d.ts +9 -0
  121. package/types/src/files/kinds.d.ts +101 -0
  122. package/types/src/files/scan.d.ts +78 -0
  123. package/types/src/files/sniff.d.ts +7 -0
  124. package/types/src/files/streams.d.ts +17 -0
  125. package/types/src/files/treatment.d.ts +18 -0
  126. package/types/src/from-row.d.ts +20 -0
  127. package/types/src/honeypot.d.ts +2 -0
  128. package/types/src/implement/anonymous.d.ts +28 -0
  129. package/types/src/implement/attachment.d.ts +14 -0
  130. package/types/src/implement/boot.d.ts +120 -0
  131. package/types/src/implement/bounded.d.ts +32 -0
  132. package/types/src/implement/browser-config.d.ts +19 -0
  133. package/types/src/implement/capability.d.ts +144 -0
  134. package/types/src/implement/challenge.d.ts +75 -0
  135. package/types/src/implement/client-address.d.ts +11 -0
  136. package/types/src/implement/connect.d.ts +67 -0
  137. package/types/src/implement/crashes.d.ts +31 -0
  138. package/types/src/implement/dispatch.d.ts +221 -0
  139. package/types/src/implement/egress.d.ts +49 -0
  140. package/types/src/implement/environment.d.ts +14 -0
  141. package/types/src/implement/failures.d.ts +39 -0
  142. package/types/src/implement/faults.d.ts +45 -0
  143. package/types/src/implement/headers.d.ts +7 -0
  144. package/types/src/implement/implement.d.ts +58 -0
  145. package/types/src/implement/index.d.ts +25 -0
  146. package/types/src/implement/invocation.d.ts +125 -0
  147. package/types/src/implement/membership.d.ts +201 -0
  148. package/types/src/implement/multipart.d.ts +36 -0
  149. package/types/src/implement/params.d.ts +7 -0
  150. package/types/src/implement/principal.d.ts +57 -0
  151. package/types/src/implement/project.d.ts +11 -0
  152. package/types/src/implement/public.d.ts +61 -0
  153. package/types/src/implement/serve.d.ts +25 -0
  154. package/types/src/implement/terminals.d.ts +33 -0
  155. package/types/src/implement/tokens.d.ts +49 -0
  156. package/types/src/implement/types.d.ts +243 -0
  157. package/types/src/implement/urls.d.ts +15 -0
  158. package/types/src/implement/verifiers.d.ts +19 -0
  159. package/types/src/implement/webhook.d.ts +55 -0
  160. package/types/src/implement/workload.d.ts +56 -0
  161. package/types/src/implement/xlsx.d.ts +6 -0
  162. package/types/src/index.d.ts +12 -0
  163. package/types/src/jobs/exception.d.ts +40 -0
  164. package/types/src/jobs/mail.d.ts +67 -0
  165. package/types/src/jobs/report.d.ts +56 -0
  166. package/types/src/jobs/runner.d.ts +154 -0
  167. package/types/src/jobs/schedule.d.ts +17 -0
  168. package/types/src/jobs/scrub.d.ts +12 -0
  169. package/types/src/jobs/types.d.ts +98 -0
  170. package/types/src/react/caller.d.ts +30 -0
  171. package/types/src/react/errors.d.ts +40 -0
  172. package/types/src/react/hooks.d.ts +110 -0
  173. package/types/src/react/index.d.ts +5 -0
  174. package/types/src/react/provider.d.ts +58 -0
  175. package/types/src/react/store.d.ts +68 -0
  176. package/types/src/react/testing.d.ts +28 -0
  177. package/types/src/react/types.d.ts +33 -0
  178. package/types/src/rpc/builder.d.ts +52 -0
  179. package/types/src/rpc/contract.d.ts +23 -0
  180. package/types/src/rpc/dispatcher.d.ts +11 -0
  181. package/types/src/rpc/types.d.ts +62 -0
  182. package/types/src/service/define.d.ts +42 -0
  183. package/types/src/service/index.d.ts +3 -0
  184. package/types/src/service/serve.d.ts +28 -0
  185. package/types/src/spa-fallback.d.ts +2 -0
  186. package/types/src/standard-schema.d.ts +55 -0
  187. package/types/src/testing/caller.d.ts +95 -0
  188. package/types/src/testing/challenge.d.ts +11 -0
  189. package/types/src/testing/dev.d.ts +97 -0
  190. package/types/src/testing/endpoint-access.d.ts +131 -0
  191. package/types/src/testing/index.d.ts +12 -0
  192. package/types/src/testing/isolation.d.ts +3 -0
  193. package/types/src/testing/kit.d.ts +187 -0
  194. package/types/src/testing/membership.d.ts +132 -0
  195. package/types/src/testing/policy-isolation.d.ts +113 -0
  196. package/types/src/testing/preload.d.ts +1 -0
  197. package/types/src/testing/proxy.d.ts +40 -0
  198. package/types/src/testing/runner.d.ts +8 -0
  199. package/types/src/testing/sample-file.d.ts +5 -0
  200. package/types/src/testing/status.d.ts +4 -0
  201. package/types/src/undumpable.d.ts +5 -0
@@ -16,6 +16,7 @@ import {
16
16
  } from '@fabrika/auth-core'
17
17
  import type { TokenVerifier } from '@fabrika/jwks'
18
18
  import { z } from 'zod/v4'
19
+ import { deadline } from './bounded'
19
20
 
20
21
  export type WorkloadKindOf = OwnWorkloadKind
21
22
 
@@ -39,20 +40,30 @@ export function issuerWorkloadTokens(
39
40
  ): WorkloadTokenSource {
40
41
  return {
41
42
  async exchange(input) {
42
- const response = await fetcher(
43
- new Request(new URL(WORKLOAD_TOKEN_PATH, issuer), {
44
- method: 'POST',
45
- headers: { authorization: `Bearer ${input.key}`, 'content-type': 'application/json' },
46
- body: JSON.stringify({ app: input.app }),
47
- redirect: 'manual',
48
- signal: AbortSignal.timeout(EXCHANGE_TIMEOUT_MS),
49
- }),
50
- )
51
- const parsed = response.status === 200 ? mintResultSchema.safeParse(await response.json()) : null
52
- if (parsed === null || !parsed.success) {
53
- throw new Error(`IAM answered the workload token exchange with ${response.status}`)
43
+ const timer = deadline(EXCHANGE_TIMEOUT_MS)
44
+ try {
45
+ const response = await fetcher(
46
+ new Request(new URL(WORKLOAD_TOKEN_PATH, issuer), {
47
+ method: 'POST',
48
+ headers: { authorization: `Bearer ${input.key}`, 'content-type': 'application/json' },
49
+ body: JSON.stringify({ app: input.app }),
50
+ redirect: 'manual',
51
+ signal: timer.signal,
52
+ }),
53
+ )
54
+ if (response.status !== 200) {
55
+ // Nothing reads a refusal's body, and once the deadline is cleared nothing else would end its stream.
56
+ await response.body?.cancel().catch(() => {})
57
+ throw new Error(`IAM answered the workload token exchange with ${response.status}`)
58
+ }
59
+ const parsed = mintResultSchema.safeParse(await response.json())
60
+ if (!parsed.success) {
61
+ throw new Error(`IAM answered the workload token exchange with ${response.status}`)
62
+ }
63
+ return parsed.data
64
+ } finally {
65
+ timer.clear()
54
66
  }
55
- return parsed.data
56
67
  },
57
68
  }
58
69
  }
@@ -3,6 +3,7 @@
3
3
  // groups a framework report by its fingerprint and raises its new-issue and regression alerts, so each kind of
4
4
  // failure becomes one issue. What was thrown travels scrubbed as the report's cause chain (ADR-0059).
5
5
 
6
+ import { deadline } from '../implement/bounded'
6
7
  import { exceptionChain, type ReportedException } from './exception'
7
8
  import type { Scrubber } from './scrub'
8
9
 
@@ -136,6 +137,7 @@ export function crashEnvelope(kind: CrashKind, error: unknown, options: Envelope
136
137
 
137
138
  async function deliver(options: OperationsReporterOptions, body: string): Promise<void> {
138
139
  const send = options.fetch ?? ((request: Request) => fetch(request))
140
+ const timer = deadline(DELIVERY_TIMEOUT_MS)
139
141
  const request = new Request(options.target.envelopeUrl, {
140
142
  method: 'POST',
141
143
  headers: {
@@ -143,7 +145,7 @@ async function deliver(options: OperationsReporterOptions, body: string): Promis
143
145
  'x-sentry-auth': `Sentry sentry_version=7, sentry_key=${options.target.publicKey}`,
144
146
  },
145
147
  body,
146
- signal: AbortSignal.timeout(DELIVERY_TIMEOUT_MS),
148
+ signal: timer.signal,
147
149
  })
148
150
  try {
149
151
  const response = await send(request)
@@ -154,6 +156,8 @@ async function deliver(options: OperationsReporterOptions, body: string): Promis
154
156
  } catch {
155
157
  // The error may quote the ingest URL with its key.
156
158
  console.error('fabrika: a report to Operations could not be delivered')
159
+ } finally {
160
+ timer.clear()
157
161
  }
158
162
  }
159
163
 
@@ -10,6 +10,7 @@ import { type ContractArtifact, JOB_SITE_PREFIX, type JobArtifact, type JobMaile
10
10
  import type { CapabilityAuthority } from '@fabrika/db/capability'
11
11
  import { type DataAccess, type FrameworkTables, type JobLeases, type LeaseFence, LeaseLostError, type Session } from '@fabrika/db/framework'
12
12
  import { EmailDeliveryError, type EmailSenderCapabilities, mayRetryDelivery } from '@fabrika/email'
13
+ import { uuidv7 } from '@fabrika/platform'
13
14
  import { NotFoundError } from '../errors'
14
15
  import { FileWork, type StoredFileKinds, storedFilesHandle } from '../files/kinds'
15
16
  import {
@@ -21,7 +22,7 @@ import {
21
22
  runInRequest,
22
23
  type Within,
23
24
  } from '../implement/capability'
24
- import { runAsSite } from '../implement/egress'
25
+ import { type PublicEgressRules, runAsSite, siteEgress } from '../implement/egress'
25
26
  import { failureCode, type FailureReason, failureReason } from '../implement/failures'
26
27
  import { ServerFault } from '../implement/faults'
27
28
  import type { ContractDatabase } from '../implement/implement'
@@ -117,6 +118,8 @@ export interface FrameworkJob {
117
118
  export interface ContractJobs {
118
119
  /** The contract's `c.cron` jobs; `run` also takes the name of a framework job the contract needs. */
119
120
  readonly names: readonly string[]
121
+ /** Every job bound to run, the framework's included, with its schedule: what a minute tick runs when due. */
122
+ readonly scheduled: readonly { readonly name: string; readonly schedule: CronSchedule }[]
120
123
  run(name: string, options?: JobRunOptions): Promise<JobRunResult>
121
124
  /**
122
125
  * Registers every job (its first-seen time), reports a run that crashed and a job that has not succeeded within
@@ -185,6 +188,8 @@ interface RunnerContext {
185
188
  /** The environment's signed tokens: `ctx.tokens` of a job that declares `signs`. */
186
189
  readonly tokens: SignedTokens | null
187
190
  readonly url: AppUrl
191
+ /** What a job that declares `egress: 'public'` reaches; such a job reaches nothing without it. */
192
+ readonly publicEgress: PublicEgressRules | null
188
193
  }
189
194
 
190
195
  /** Where `fabrika app dev` shows what a handler threw, on the developer's own machine; nothing else sets one. */
@@ -224,6 +229,8 @@ export interface JobBindingInput {
224
229
  readonly tokens?: SignedTokens
225
230
  /** `ctx.url`: absolute links to the application's own procedures; without it every link is refused. */
226
231
  readonly url?: AppUrl
232
+ /** The rules of public egress, which `boot` reads from the environment; without them a public job reaches nothing. */
233
+ readonly publicEgress?: PublicEgressRules
227
234
  }
228
235
 
229
236
  interface BoundFrameworkJob {
@@ -323,11 +330,13 @@ export function bindJobs(input: JobBindingInput, problems: string[]): BoundJobs
323
330
  mail: input.mail,
324
331
  tokens: input.tokens ?? null,
325
332
  url: input.url ?? appUrl(input.artifact, null),
333
+ publicEgress: input.publicEgress ?? null,
326
334
  // A job's kicks run only after the job is bound, so `kick` below is defined by then.
327
335
  kick: (name: string) => kick(name),
328
336
  })
329
337
  const jobs: ContractJobs = Object.freeze({
330
338
  names,
339
+ scheduled: Object.freeze([...bound.values(), ...frameworkJobs.values()].map(({ name, schedule }) => ({ name, schedule }))),
331
340
  run(name: string, options: JobRunOptions = {}): Promise<JobRunResult> {
332
341
  const own = frameworkJobs.get(name)
333
342
  if (own !== undefined && framework !== null) {
@@ -432,7 +441,7 @@ function jobLinks(where: string, site: string, declaration: SiteDeclaration | nu
432
441
  return { name, prefix: mount.prefix, dimension: mount.dimension, issuedBy: mount.issuedBy }
433
442
  }
434
443
 
435
- const attemptId = (): string => Bun.randomUUIDv7()
444
+ const attemptId = (): string => uuidv7()
436
445
 
437
446
  /** Exponential from one second, capped at five minutes, with half of it jittered. */
438
447
  function backoffMs(attempt: number, random: number): number {
@@ -694,7 +703,7 @@ async function attemptOnce(
694
703
  if (site === null) {
695
704
  await job.handler({ ctx })
696
705
  } else {
697
- await runAsSite(`job '${job.name}'`, site.egress, async () => await job.handler({ ctx }))
706
+ await runAsSite(`job '${job.name}'`, siteEgress(site.egress, context.publicEgress), async () => await job.handler({ ctx }))
698
707
  }
699
708
  kicked.push(...kicks.kicked())
700
709
  }).then(() => null, (error: unknown) => {
@@ -898,25 +907,37 @@ function kitJobContext(
898
907
  db: siteData(reads, (action) => reads.app(app(action))),
899
908
  sql: site.sql === null ? undeclaredSql(job, 'job') : jobSqlOutsideTransaction(job),
900
909
  files: putAndOpen(boundTo(frame, () => false, 'ctx.files')),
901
- transaction: async <R>(work: (tx: object) => Promise<R>): Promise<R> => {
910
+ transaction: async <R>(body: (tx: object) => Promise<R>): Promise<R> => {
902
911
  // Outside its attempt there is no lease to fence the transaction with, so it never opens.
903
912
  boundTo(frame, () => false)()
904
- return data.transaction(async (session) => {
905
- let open = true
906
- const writes = jobHandles(frame, data, site, principal, 'mutation', async () => session, () => !open, undefined, binding)
907
- try {
908
- const sql = site.sql === null ? undeclaredSql(job, 'job', 'tx.sql') : jobSql(frame, data, site.sql, session, () => !open)
909
- // Located in this transaction, so an object it wrote is found before the commit.
910
- const located = (action: string, dimension: string, id: string) => {
911
- boundTo(frame, () => !open, 'tx.ref')()
912
- return ref(action, dimension, id, session)
913
+ // Its own files: a rollback releases only what this transaction staged, never a file of a concurrent one.
914
+ const own = new FileWork(frame, frame.id)
915
+ let rolledBack = false
916
+ try {
917
+ return await data.transaction(async (session) => {
918
+ let open = true
919
+ const writes = jobHandles(frame, data, site, principal, 'mutation', async () => session, () => !open, undefined, binding)
920
+ try {
921
+ const sql = site.sql === null ? undeclaredSql(job, 'job', 'tx.sql') : jobSql(frame, data, site.sql, session, () => !open)
922
+ // Located in this transaction, so an object it wrote is found before the commit.
923
+ const located = (action: string, dimension: string, id: string) => {
924
+ boundTo(frame, () => !open, 'tx.ref')()
925
+ return ref(action, dimension, id, session)
926
+ }
927
+ const files = storedFilesHandle(kinds, own, { where: job, check: boundTo(frame, () => !open, 'tx.files'), writes: true })
928
+ return await body(Object.freeze({ db: siteData(writes, (action) => writes.app(app(action))), sql, files, ref: located }))
929
+ } catch (error) {
930
+ rolledBack = true
931
+ throw error
932
+ } finally {
933
+ open = false
913
934
  }
914
- const files = putAndOpen(boundTo(frame, () => !open, 'tx.files'))
915
- return await work(Object.freeze({ db: siteData(writes, (action) => writes.app(app(action))), sql, files, ref: located }))
916
- } finally {
917
- open = false
918
- }
919
- })
935
+ })
936
+ } catch (error) {
937
+ // A transaction the work threw out of is a known rollback; a failed commit is not, and the sweep decides.
938
+ if (rolledBack) await kinds?.release(own)
939
+ throw error
940
+ }
920
941
  },
921
942
  }
922
943
  }
@@ -1,5 +1,6 @@
1
1
  // The five-field cron expressions of `c.cron`, read in UTC: which minutes a job is due, so the runner can tell how
2
- // long ago it should last have succeeded. The crontab, not this module, triggers a run.
2
+ // long ago it should last have succeeded. On Bun the crontab triggers a run; on Cloudflare the application's object
3
+ // runs, at each minute tick, the jobs `isDueAt` that minute.
3
4
 
4
5
  interface Field {
5
6
  readonly min: number
@@ -96,6 +97,11 @@ function dayMatches(schedule: CronSchedule, day: Date): boolean {
96
97
  return schedule.eitherDay ? byMonth || byWeek : byMonth && byWeek
97
98
  }
98
99
 
100
+ /** Whether the schedule is due in the UTC minute that holds `at`: what a Worker's minute tick runs (ADR-0065). */
101
+ export function isDueAt(schedule: CronSchedule, at: Date): boolean {
102
+ return schedule.minutes.has(at.getUTCMinutes()) && schedule.hours.has(at.getUTCHours()) && dayMatches(schedule, at)
103
+ }
104
+
99
105
  const descending = (values: ReadonlySet<number>): number[] => [...values].sort((left, right) => right - left)
100
106
 
101
107
  /** The latest `count` due minutes at or before `at`, newest first; fewer when the schedule is that rare. */
package/src/jobs/scrub.ts CHANGED
@@ -1,7 +1,16 @@
1
1
  // What an error report must not carry (ADR-0059): the values of the application's secrets, and anything shaped like a
2
2
  // credential — a JWT, an `Authorization` scheme or header, a URL's userinfo, a sensitive query parameter.
3
3
 
4
- import { FABRIKA_CONTRACT_DIGEST, FABRIKA_IAM_APP_ID, FABRIKA_IAM_ISSUER, PLATFORM_RECEIVERS, platformTransportVariables } from '@fabrika/auth-core'
4
+ import {
5
+ FABRIKA_APPLICATION_OBJECT,
6
+ FABRIKA_APPLICATION_OBJECTS,
7
+ FABRIKA_CONTRACT_DIGEST,
8
+ FABRIKA_IAM_APP_ID,
9
+ FABRIKA_IAM_ISSUER,
10
+ FABRIKA_OPS_KEY_DIGESTS,
11
+ PLATFORM_RECEIVERS,
12
+ platformTransportVariables,
13
+ } from '@fabrika/auth-core'
5
14
  import type { CapturedEnvironment } from '../implement/environment'
6
15
 
7
16
  export const REDACTED = '[redacted]'
@@ -21,6 +30,10 @@ const PUBLIC_FRAMEWORK_VARIABLES: ReadonlySet<string> = new Set([
21
30
  FABRIKA_CONTRACT_DIGEST,
22
31
  FABRIKA_IAM_APP_ID,
23
32
  FABRIKA_IAM_ISSUER,
33
+ // Digests of the ops keys, which are no credential; the object names are words (`application`) a message carries.
34
+ FABRIKA_OPS_KEY_DIGESTS,
35
+ FABRIKA_APPLICATION_OBJECT,
36
+ FABRIKA_APPLICATION_OBJECTS,
24
37
  ...PLATFORM_RECEIVERS.map((receiver) => platformTransportVariables(receiver).url),
25
38
  ])
26
39
 
@@ -0,0 +1,12 @@
1
+ // Which request an SPA mount answers with its `index.html` when it names no file: a route of the page, not a missing
2
+ // asset. Both asset servers apply it (`diskAssets`, `bindingAssets`), so a missing script stays a 404 and never arrives
3
+ // as an HTML page with a 200.
4
+
5
+ import { rawRequestTarget } from '@fabrika/auth-core'
6
+
7
+ /** A GET or HEAD whose last path segment has no extension, or one that asks for HTML (a navigation to `/notes/v1.2`). */
8
+ export function isPageRoute(request: Request): boolean {
9
+ if (request.method !== 'GET' && request.method !== 'HEAD') return false
10
+ const last = rawRequestTarget(request.url).pathname.split('/').pop() ?? ''
11
+ return !last.includes('.') || (request.headers.get('Accept') ?? '').includes('text/html')
12
+ }