@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.
- package/package.json +40 -24
- package/skill/SKILL.md +29 -3
- package/skill/references/app-setup.md +29 -11
- package/skill/references/dev.md +2 -1
- package/skill/references/endpoint.md +12 -3
- package/skill/references/errors.md +39 -35
- package/skill/references/files.md +4 -2
- package/skill/references/jobs.md +9 -0
- package/skill/references/table-and-migration.md +19 -2
- package/skill/references/tests.md +33 -1
- package/skill/references/webhooks-links-connectors.md +1 -1
- package/src/build-recipe.ts +198 -0
- package/src/build.ts +60 -4
- package/src/bun.ts +33 -8
- package/src/cloudflare/application.ts +352 -0
- package/src/cloudflare/bindings.ts +159 -0
- package/src/cloudflare/names.ts +16 -0
- package/src/cloudflare/ops.ts +246 -0
- package/src/cloudflare/r2-files.ts +172 -0
- package/src/cloudflare.ts +99 -146
- package/src/contract/app-id.ts +5 -0
- package/src/contract/artifact-schema.ts +8 -1
- package/src/contract/build.ts +68 -11
- package/src/contract/compile.ts +1 -0
- package/src/contract/context.ts +9 -0
- package/src/contract/declaration.ts +2 -0
- package/src/contract/define.ts +28 -2
- package/src/contract/group.ts +10 -1
- package/src/contract/kit.ts +6 -2
- package/src/contract/platform.ts +3 -2
- package/src/contract/vocabulary.ts +26 -8
- package/src/dev/changes.ts +35 -0
- package/src/dev/contract-digest.ts +17 -0
- package/src/dev/index.ts +11 -0
- package/src/dev/launcher.ts +713 -0
- package/src/dev/output.ts +72 -0
- package/src/dev/wire.ts +136 -0
- package/src/disk-assets.ts +8 -3
- package/src/files/archive.ts +2 -2
- package/src/files/job-names.ts +12 -0
- package/src/files/kinds.ts +19 -7
- package/src/files/scan.ts +3 -4
- package/src/files/streams.ts +3 -1
- package/src/implement/anonymous.ts +11 -1
- package/src/implement/boot.ts +70 -7
- package/src/implement/bounded.ts +21 -2
- package/src/implement/capability.ts +18 -3
- package/src/implement/dispatch.ts +251 -147
- package/src/implement/egress.ts +341 -8
- package/src/implement/failures.ts +17 -1
- package/src/implement/faults.ts +1 -0
- package/src/implement/index.ts +1 -0
- package/src/implement/invocation.ts +18 -9
- package/src/implement/principal.ts +25 -2
- package/src/implement/terminals.ts +93 -0
- package/src/implement/workload.ts +24 -13
- package/src/jobs/report.ts +5 -1
- package/src/jobs/runner.ts +40 -19
- package/src/jobs/schedule.ts +7 -1
- package/src/jobs/scrub.ts +14 -1
- package/src/spa-fallback.ts +12 -0
- package/src/testing/endpoint-access.ts +307 -152
- package/src/testing/isolation.ts +8 -1
- package/src/testing/kit.ts +20 -2
- package/src/testing/proxy.ts +5 -5
- package/types/src/agent-skill.d.ts +29 -0
- package/types/src/build-recipe.d.ts +70 -0
- package/types/src/build.d.ts +29 -0
- package/types/src/bun.d.ts +66 -0
- package/types/src/client/caller.d.ts +19 -0
- package/types/src/client/contract.d.ts +131 -0
- package/types/src/client/error-reporting.d.ts +66 -0
- package/types/src/client/index.d.ts +4 -0
- package/types/src/client/rpc.d.ts +14 -0
- package/types/src/client/share.d.ts +2 -0
- package/types/src/client/transport.d.ts +42 -0
- package/types/src/cloudflare/application.d.ts +42 -0
- package/types/src/cloudflare/bindings.d.ts +33 -0
- package/types/src/cloudflare/names.d.ts +11 -0
- package/types/src/cloudflare/ops.d.ts +46 -0
- package/types/src/cloudflare/r2-files.d.ts +9 -0
- package/types/src/cloudflare.d.ts +42 -0
- package/types/src/contract/app-id.d.ts +2 -0
- package/types/src/contract/artifact-schema.d.ts +8 -0
- package/types/src/contract/attachment.d.ts +41 -0
- package/types/src/contract/build.d.ts +12 -0
- package/types/src/contract/compile.d.ts +11 -0
- package/types/src/contract/connect.d.ts +51 -0
- package/types/src/contract/context.d.ts +510 -0
- package/types/src/contract/declaration.d.ts +307 -0
- package/types/src/contract/defaults.d.ts +208 -0
- package/types/src/contract/define.d.ts +376 -0
- package/types/src/contract/file.d.ts +44 -0
- package/types/src/contract/group.d.ts +282 -0
- package/types/src/contract/handled.d.ts +18 -0
- package/types/src/contract/index.d.ts +19 -0
- package/types/src/contract/kit.d.ts +211 -0
- package/types/src/contract/platform.d.ts +9 -0
- package/types/src/contract/problems.d.ts +14 -0
- package/types/src/contract/redirect.d.ts +25 -0
- package/types/src/contract/schema-from-shape.d.ts +10 -0
- package/types/src/contract/schema-shape.d.ts +5 -0
- package/types/src/contract/snapshot-source.d.ts +15 -0
- package/types/src/contract/token.d.ts +58 -0
- package/types/src/contract/vocabulary.d.ts +32 -0
- package/types/src/contract/webhook-binding.d.ts +24 -0
- package/types/src/contract/webhook-scheme.d.ts +23 -0
- package/types/src/dev/changes.d.ts +7 -0
- package/types/src/dev/contract-digest.d.ts +1 -0
- package/types/src/dev/index.d.ts +2 -0
- package/types/src/dev/launcher.d.ts +32 -0
- package/types/src/dev/output.d.ts +28 -0
- package/types/src/dev/wire.d.ts +126 -0
- package/types/src/disk-assets.d.ts +6 -0
- package/types/src/errors.d.ts +46 -0
- package/types/src/fetch.d.ts +7 -0
- package/types/src/files/archive.d.ts +26 -0
- package/types/src/files/clamscan.d.ts +32 -0
- package/types/src/files/handle.d.ts +106 -0
- package/types/src/files/job-names.d.ts +9 -0
- package/types/src/files/kinds.d.ts +101 -0
- package/types/src/files/scan.d.ts +78 -0
- package/types/src/files/sniff.d.ts +7 -0
- package/types/src/files/streams.d.ts +17 -0
- package/types/src/files/treatment.d.ts +18 -0
- package/types/src/from-row.d.ts +20 -0
- package/types/src/honeypot.d.ts +2 -0
- package/types/src/implement/anonymous.d.ts +28 -0
- package/types/src/implement/attachment.d.ts +14 -0
- package/types/src/implement/boot.d.ts +120 -0
- package/types/src/implement/bounded.d.ts +32 -0
- package/types/src/implement/browser-config.d.ts +19 -0
- package/types/src/implement/capability.d.ts +144 -0
- package/types/src/implement/challenge.d.ts +75 -0
- package/types/src/implement/client-address.d.ts +11 -0
- package/types/src/implement/connect.d.ts +67 -0
- package/types/src/implement/crashes.d.ts +31 -0
- package/types/src/implement/dispatch.d.ts +221 -0
- package/types/src/implement/egress.d.ts +49 -0
- package/types/src/implement/environment.d.ts +14 -0
- package/types/src/implement/failures.d.ts +39 -0
- package/types/src/implement/faults.d.ts +45 -0
- package/types/src/implement/headers.d.ts +7 -0
- package/types/src/implement/implement.d.ts +58 -0
- package/types/src/implement/index.d.ts +25 -0
- package/types/src/implement/invocation.d.ts +125 -0
- package/types/src/implement/membership.d.ts +201 -0
- package/types/src/implement/multipart.d.ts +36 -0
- package/types/src/implement/params.d.ts +7 -0
- package/types/src/implement/principal.d.ts +57 -0
- package/types/src/implement/project.d.ts +11 -0
- package/types/src/implement/public.d.ts +61 -0
- package/types/src/implement/serve.d.ts +25 -0
- package/types/src/implement/terminals.d.ts +33 -0
- package/types/src/implement/tokens.d.ts +49 -0
- package/types/src/implement/types.d.ts +243 -0
- package/types/src/implement/urls.d.ts +15 -0
- package/types/src/implement/verifiers.d.ts +19 -0
- package/types/src/implement/webhook.d.ts +55 -0
- package/types/src/implement/workload.d.ts +56 -0
- package/types/src/implement/xlsx.d.ts +6 -0
- package/types/src/index.d.ts +12 -0
- package/types/src/jobs/exception.d.ts +40 -0
- package/types/src/jobs/mail.d.ts +67 -0
- package/types/src/jobs/report.d.ts +56 -0
- package/types/src/jobs/runner.d.ts +154 -0
- package/types/src/jobs/schedule.d.ts +17 -0
- package/types/src/jobs/scrub.d.ts +12 -0
- package/types/src/jobs/types.d.ts +98 -0
- package/types/src/react/caller.d.ts +30 -0
- package/types/src/react/errors.d.ts +40 -0
- package/types/src/react/hooks.d.ts +110 -0
- package/types/src/react/index.d.ts +5 -0
- package/types/src/react/provider.d.ts +58 -0
- package/types/src/react/store.d.ts +68 -0
- package/types/src/react/testing.d.ts +28 -0
- package/types/src/react/types.d.ts +33 -0
- package/types/src/rpc/builder.d.ts +52 -0
- package/types/src/rpc/contract.d.ts +23 -0
- package/types/src/rpc/dispatcher.d.ts +11 -0
- package/types/src/rpc/types.d.ts +62 -0
- package/types/src/service/define.d.ts +42 -0
- package/types/src/service/index.d.ts +3 -0
- package/types/src/service/serve.d.ts +28 -0
- package/types/src/spa-fallback.d.ts +2 -0
- package/types/src/standard-schema.d.ts +55 -0
- package/types/src/testing/caller.d.ts +95 -0
- package/types/src/testing/challenge.d.ts +11 -0
- package/types/src/testing/dev.d.ts +97 -0
- package/types/src/testing/endpoint-access.d.ts +131 -0
- package/types/src/testing/index.d.ts +12 -0
- package/types/src/testing/isolation.d.ts +3 -0
- package/types/src/testing/kit.d.ts +187 -0
- package/types/src/testing/membership.d.ts +132 -0
- package/types/src/testing/policy-isolation.d.ts +113 -0
- package/types/src/testing/preload.d.ts +1 -0
- package/types/src/testing/proxy.d.ts +40 -0
- package/types/src/testing/runner.d.ts +8 -0
- package/types/src/testing/sample-file.d.ts +5 -0
- package/types/src/testing/status.d.ts +4 -0
- package/types/src/undumpable.d.ts +5 -0
|
@@ -34,6 +34,9 @@ export const CAPABILITY_REFUSALS = {
|
|
|
34
34
|
undeclared_binding: 'the site named a webhook it does not declare in binds',
|
|
35
35
|
undeclared_signs: 'the site signed a token of a purpose it does not declare in signs, or for longer than the purpose allows',
|
|
36
36
|
undeclared_egress: 'the site called an origin it does not declare in egress',
|
|
37
|
+
refused_egress:
|
|
38
|
+
"the site's public egress refused the host: an IP address, a local or internal name, a non-default port, a host of this installation, or a name that resolves to a private address",
|
|
39
|
+
oversized_egress: "a response to the site's public egress was larger than it reads",
|
|
37
40
|
wrong_binding: 'a webhook binding was handed a capability of another kind or dimension than the webhook is bound to',
|
|
38
41
|
missing_id: 'the job named an object without an id',
|
|
39
42
|
wrong_capability: 'an operation was handed a capability of another dimension or action than it needs',
|
|
@@ -269,14 +272,18 @@ export class Within<D extends string, A extends string> implements WithinCapabil
|
|
|
269
272
|
principal: string,
|
|
270
273
|
values: readonly string[] | 'all',
|
|
271
274
|
containers: readonly ContainerGrant[],
|
|
275
|
+
contained: readonly ContainerGrant[],
|
|
272
276
|
) {
|
|
273
277
|
if (token !== MINT) {
|
|
274
278
|
throw new CapabilityRefusedError('not_minted', 'a Within is minted by the framework only; declare the access form in fabrika.contract.ts')
|
|
275
279
|
}
|
|
276
280
|
const held = values === 'all' ? freeze([]) : containerGrantsOf(containers)
|
|
281
|
+
const inside = values === 'all' ? freeze([]) : containerGrantsOf(contained)
|
|
277
282
|
let count = values === 'all' ? 0 : values.length
|
|
278
|
-
for (
|
|
279
|
-
|
|
283
|
+
for (const group of [held, inside]) {
|
|
284
|
+
for (let index = 0; index < group.length; index++) {
|
|
285
|
+
count += group[index]?.values.length ?? 0
|
|
286
|
+
}
|
|
280
287
|
}
|
|
281
288
|
if (count > MAX_WITHIN_VALUES) {
|
|
282
289
|
throw new CapabilityLimitError(
|
|
@@ -292,6 +299,7 @@ export class Within<D extends string, A extends string> implements WithinCapabil
|
|
|
292
299
|
principal,
|
|
293
300
|
values: values === 'all' ? 'all' : copyOf(values),
|
|
294
301
|
containers: held,
|
|
302
|
+
contained: inside,
|
|
295
303
|
})
|
|
296
304
|
this.#frame = frame
|
|
297
305
|
this.#issuer = issuer
|
|
@@ -315,6 +323,11 @@ export class Within<D extends string, A extends string> implements WithinCapabil
|
|
|
315
323
|
return this.#grant.containers
|
|
316
324
|
}
|
|
317
325
|
|
|
326
|
+
/** The dimensions inside this one in which the caller holds the action: `note → [n1]` for a workspace list. */
|
|
327
|
+
get contained(): readonly ContainerGrant[] {
|
|
328
|
+
return this.#grant.contained
|
|
329
|
+
}
|
|
330
|
+
|
|
318
331
|
static {
|
|
319
332
|
readWithin = (value) =>
|
|
320
333
|
typeof value === 'object' && value !== null && #grant in value ? { grant: value.#grant, frame: value.#frame, issuer: value.#issuer } : undefined
|
|
@@ -434,6 +447,7 @@ export interface CapabilityMint {
|
|
|
434
447
|
principal: string,
|
|
435
448
|
values: readonly string[] | 'all',
|
|
436
449
|
containers?: readonly ContainerGrant[],
|
|
450
|
+
contained?: readonly ContainerGrant[],
|
|
437
451
|
): Within<D, A>
|
|
438
452
|
app<A extends string>(frame: RequestFrame, action: A, principal: string): AppCapability<A>
|
|
439
453
|
}
|
|
@@ -469,8 +483,9 @@ export function createCapabilityIssuer(): CapabilityIssuer {
|
|
|
469
483
|
principal: string,
|
|
470
484
|
values: readonly string[] | 'all',
|
|
471
485
|
containers: readonly ContainerGrant[] = [],
|
|
486
|
+
contained: readonly ContainerGrant[] = [],
|
|
472
487
|
): Within<D, A> {
|
|
473
|
-
return new Within(MINT, issuer, frame, dimension, action, principal, values, containers)
|
|
488
|
+
return new Within(MINT, issuer, frame, dimension, action, principal, values, containers, contained)
|
|
474
489
|
},
|
|
475
490
|
app<A extends string>(frame: RequestFrame, action: A, principal: string): AppCapability<A> {
|
|
476
491
|
return new AppCapability(MINT, issuer, frame, action, principal)
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
type EndpointRow,
|
|
17
17
|
type ExportDescription,
|
|
18
18
|
FORM_URLENCODED_TYPE,
|
|
19
|
+
type IamTerminalRpc,
|
|
19
20
|
isAttachmentShape,
|
|
20
21
|
isFileShape,
|
|
21
22
|
isRedirectShape,
|
|
@@ -67,9 +68,9 @@ import {
|
|
|
67
68
|
import type { RequestExecutionContext } from '../fetch'
|
|
68
69
|
import { FileWork, type StoredFileKinds, storedFilesHandle } from '../files/kinds'
|
|
69
70
|
import { FileDownload, fileResponse, withFileTreatment } from '../files/treatment'
|
|
70
|
-
import { abortedBody, bodyDeadlineMs, bootstrapPath, bootstrapResponse,
|
|
71
|
+
import { abortedBody, bodyDeadlineMs, bootstrapPath, bootstrapResponse, readBodyInTime, withBootstrap } from './anonymous'
|
|
71
72
|
import { attachmentResponse } from './attachment'
|
|
72
|
-
import { boundedStream, handlerSignal, readBoundedBytes } from './bounded'
|
|
73
|
+
import { boundedStream, type Deadline, deadline, handlerSignal, readBoundedBytes } from './bounded'
|
|
73
74
|
import type { BrowserConfig } from './browser-config'
|
|
74
75
|
import {
|
|
75
76
|
type AppCapability,
|
|
@@ -83,7 +84,7 @@ import {
|
|
|
83
84
|
} from './capability'
|
|
84
85
|
import type { ChallengeIssuer, ChallengeVerifier } from './challenge'
|
|
85
86
|
import { connectContext, type ImportWire } from './connect'
|
|
86
|
-
import { runAsSite } from './egress'
|
|
87
|
+
import { runAsSite, type SiteEgress, siteEgress } from './egress'
|
|
87
88
|
import type { FailureReports } from './failures'
|
|
88
89
|
import { ServerFault } from './faults'
|
|
89
90
|
import { type HeaderPolicy, withHeaderPolicy } from './headers'
|
|
@@ -104,6 +105,7 @@ import { paramsObject, urlParams } from './params'
|
|
|
104
105
|
import { pageCallerOf, presentedBy, type Principal } from './principal'
|
|
105
106
|
import { project, ProjectionRefusedError } from './project'
|
|
106
107
|
import { bucketsOf, DEFAULT_ADMISSION, type InFlightShare, type ProcedureAdmission, requestClient } from './public'
|
|
108
|
+
import { type TerminalIdentification, terminalIdentification } from './terminals'
|
|
107
109
|
import type { SignedTokens } from './tokens'
|
|
108
110
|
import type { AppUrl } from './urls'
|
|
109
111
|
import { githubAccounts, verifyDelivery } from './verifiers'
|
|
@@ -165,7 +167,7 @@ export type ServedEndpoint =
|
|
|
165
167
|
/** A staged-kit site, which gets `ctx.db`; a handler `implement` bound has none. */
|
|
166
168
|
readonly site?: ServedSite
|
|
167
169
|
/** The origins a staged-kit site's `fetch` reaches while its handler runs; absent for a handler `implement` bound. */
|
|
168
|
-
readonly egress?:
|
|
170
|
+
readonly egress?: SiteEgress
|
|
169
171
|
/** Its buckets and time limit; without them (a generated endpoint) no bucket and the request budget. */
|
|
170
172
|
readonly admission?: ProcedureAdmission
|
|
171
173
|
/** A POST's `query`, parsed from the URL beside the body: its schema and its shape. */
|
|
@@ -241,6 +243,8 @@ export interface RuntimeParts {
|
|
|
241
243
|
readonly assets: SpaAssets | null
|
|
242
244
|
/** IAM's membership operations; `boot` refuses a contract that needs them when the adapter supplies none. */
|
|
243
245
|
readonly membership: IamMembership | null
|
|
246
|
+
/** IAM's identification operations, where the contract declares a terminal; `boot` refuses one without them. */
|
|
247
|
+
readonly terminals: IamTerminalRpc | null
|
|
244
248
|
/** The framework's own writes (admission buckets, webhook tables); `boot` refuses a webhook or a bucket without them. */
|
|
245
249
|
readonly framework: FrameworkTables | null
|
|
246
250
|
/** This runtime's capability mint, which `boot` alone holds, and the authority its stores are bound to. */
|
|
@@ -518,6 +522,22 @@ export class ContractRuntime {
|
|
|
518
522
|
const admission = served.admission ?? DEFAULT_ADMISSION
|
|
519
523
|
await this.#admit(served.row, admission, request, caller.subject)
|
|
520
524
|
const body = await procedureBody(served, request, this.#parts.fileKinds)
|
|
525
|
+
try {
|
|
526
|
+
return await this.#procedureInvocation(served, request, exec, caller, admission, body)
|
|
527
|
+
} finally {
|
|
528
|
+
// Whatever refuses the request once the body is open, a multipart body's stream and deadline end here.
|
|
529
|
+
await body.multipart?.close()
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
async #procedureInvocation(
|
|
534
|
+
served: Extract<ServedEndpoint, { kind: 'procedure' }>,
|
|
535
|
+
request: Request,
|
|
536
|
+
exec: RequestExecutionContext | undefined,
|
|
537
|
+
caller: Caller,
|
|
538
|
+
admission: ProcedureAdmission,
|
|
539
|
+
body: Awaited<ReturnType<typeof procedureBody>>,
|
|
540
|
+
): Promise<Response | FileDownload> {
|
|
521
541
|
// Signed tokens are verified as the input is parsed; the texts they carry are what the request may redirect to.
|
|
522
542
|
const carried = new Set<string>()
|
|
523
543
|
const fields = this.#parts.tokens.verifyFields(served.row.input, body.multipart === null ? body.input : body.multipart.json, carried)
|
|
@@ -528,58 +548,64 @@ export class ContractRuntime {
|
|
|
528
548
|
const invocation = site === undefined ? null : new Invocation(frame, this.#data(), site.kind === 'query' || served.readOnly === true)
|
|
529
549
|
const kicks = kickRecorder(`'${served.row.id}'`, site?.declaration.kicks ?? [])
|
|
530
550
|
let outcome: 'open' | 'committing' = 'open'
|
|
551
|
+
let budget: Deadline | null = null
|
|
531
552
|
try {
|
|
532
553
|
const input = body.multipart === null
|
|
533
554
|
? fields
|
|
534
555
|
: this.#parts.tokens.verifyFields(served.row.input, await this.#fileInput(served, body.multipart, work), carried)
|
|
535
556
|
// The handler's time starts after the last body byte, so a slow upload does not eat it.
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
result
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
? {}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
557
|
+
budget = handlerSignal(request, admission.timeoutMs)
|
|
558
|
+
const { signal } = budget
|
|
559
|
+
const answer = await enclosedAnswer(invocation, this.#hold(served.row.id, exec), async () => {
|
|
560
|
+
let result: unknown
|
|
561
|
+
try {
|
|
562
|
+
result = await runInRequest(frame, async () => {
|
|
563
|
+
const membership = this.#membershipRequest(served.row, caller, request, frame)
|
|
564
|
+
const access = await this.#access(served.row, caller, input, frame, membership)
|
|
565
|
+
const binding = this.#parts.fileKinds?.binding(work)
|
|
566
|
+
const ctx = Object.freeze({
|
|
567
|
+
principal: caller.principal,
|
|
568
|
+
requestId: frame.id,
|
|
569
|
+
signal,
|
|
570
|
+
client: requestClient(request, this.#parts.clientVouched),
|
|
571
|
+
url: this.#parts.url,
|
|
572
|
+
...access.context,
|
|
573
|
+
...this.#facilities(served.facilities, served.row, membership, access.context['target'], {
|
|
574
|
+
credential: caller.token,
|
|
575
|
+
requestId: request.headers.get(REQUEST_ID_HEADER) ?? frame.id,
|
|
576
|
+
}),
|
|
577
|
+
...this.#terminals(caller.token, request.headers.get(REQUEST_ID_HEADER) ?? frame.id),
|
|
578
|
+
...(served.connect === undefined ? {} : { connect: connectContext(served.connect, frame) }),
|
|
579
|
+
...(site === undefined || invocation === null ? {} : siteContext(served.row, site, invocation, caller.subject, access.context, binding)),
|
|
580
|
+
// An export's consumer reaches its rows only, never the files of their owners.
|
|
581
|
+
...(site === undefined || invocation === null || served.readOnly === true
|
|
582
|
+
? {}
|
|
583
|
+
: { files: this.#files(served.row.id, invocation, site.kind, work) }),
|
|
584
|
+
...(served.binds === undefined || invocation === null
|
|
585
|
+
? {}
|
|
586
|
+
: { webhooks: this.#webhookBindings(served.row.id, served.binds, invocation, caller.subject) }),
|
|
587
|
+
...this.#signing(`'${served.row.id}'`, site?.declaration.signs ?? []),
|
|
588
|
+
kick: kicks.kick,
|
|
589
|
+
})
|
|
590
|
+
const args = { input: access.input, ctx, ...(query === undefined ? {} : { query }) }
|
|
591
|
+
const egress = served.egress
|
|
592
|
+
return egress === undefined
|
|
593
|
+
? await served.handler(args)
|
|
594
|
+
: await runAsSite(`'${served.row.id}'`, egress, async () => await served.handler(args))
|
|
565
595
|
})
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
}
|
|
575
|
-
const answer = await this.#answerOf(served, result, request, work, carried)
|
|
576
|
-
outcome = 'committing'
|
|
577
|
-
await commitAnswer(invocation, signal, answer)
|
|
596
|
+
} finally {
|
|
597
|
+
frame.end()
|
|
598
|
+
}
|
|
599
|
+
const answer = await this.#answerOf(served, result, request, work, carried)
|
|
600
|
+
outcome = 'committing'
|
|
601
|
+
await commitAnswer(invocation, signal, answer)
|
|
602
|
+
return answer
|
|
603
|
+
})
|
|
578
604
|
this.#kickAll(kicks.kicked(), exec)
|
|
579
605
|
return answer
|
|
580
606
|
} finally {
|
|
607
|
+
budget?.clear()
|
|
581
608
|
await invocation?.rollback()
|
|
582
|
-
await body.multipart?.close()
|
|
583
609
|
// Only a known rollback deletes what the request staged; after a commit that may have happened, the sweep decides.
|
|
584
610
|
if (outcome === 'open') {
|
|
585
611
|
await this.#parts.fileKinds?.release(work)
|
|
@@ -671,6 +697,22 @@ export class ContractRuntime {
|
|
|
671
697
|
return data
|
|
672
698
|
}
|
|
673
699
|
|
|
700
|
+
/** Where an invocation of `site` reports a long hold of its transaction: `FailureReports`, its delivery kept alive like a failure's. */
|
|
701
|
+
#hold(site: string, exec: RequestExecutionContext | undefined): Hold {
|
|
702
|
+
return {
|
|
703
|
+
site,
|
|
704
|
+
held: (heldMs) => {
|
|
705
|
+
// The transaction already settled: a report that cannot be made, kept or sent never changes the answer.
|
|
706
|
+
try {
|
|
707
|
+
const delivery = this.#parts.failures.held(site, heldMs).catch(() => undefined)
|
|
708
|
+
exec?.waitUntil(delivery)
|
|
709
|
+
} catch {
|
|
710
|
+
// Dropped, like a delivery that fails.
|
|
711
|
+
}
|
|
712
|
+
},
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
674
716
|
/** The caller's authority at IAM for this request, when the endpoint's access form names a dimension and an action. */
|
|
675
717
|
#membershipRequest(row: CredentialedEndpoint, caller: Caller, request: Request, frame: RequestFrame): MembershipRequest | null {
|
|
676
718
|
const iam = this.#parts.membership
|
|
@@ -687,6 +729,12 @@ export class ContractRuntime {
|
|
|
687
729
|
}
|
|
688
730
|
}
|
|
689
731
|
|
|
732
|
+
/** `ctx.terminals`, with the credential IAM takes the request's application from: the caller's, or the workload's. */
|
|
733
|
+
#terminals(credential: string, requestId: string): { readonly terminals?: TerminalIdentification } {
|
|
734
|
+
const rpc = this.#parts.terminals
|
|
735
|
+
return rpc === null ? {} : { terminals: terminalIdentification(rpc, { app: this.#parts.audience, credential, requestId }) }
|
|
736
|
+
}
|
|
737
|
+
|
|
690
738
|
/** `ctx.members` and `ctx.links`, for exactly the endpoints the artifact gives them. */
|
|
691
739
|
#facilities(
|
|
692
740
|
facilities: Facilities,
|
|
@@ -755,17 +803,21 @@ export class ContractRuntime {
|
|
|
755
803
|
}
|
|
756
804
|
}
|
|
757
805
|
|
|
758
|
-
/**
|
|
806
|
+
/**
|
|
807
|
+
* The values of `dimension` in which the caller holds `action`, directly or through a container, and its grants on the
|
|
808
|
+
* dimensions inside it, so a list returns every row a single-object read admits (S4); all for a global grant.
|
|
809
|
+
*/
|
|
759
810
|
#within(caller: Caller, action: string, dimension: string, frame: RequestFrame): Within<string, string> {
|
|
760
811
|
const direct = scopedValues(caller.perms, action, dimension)
|
|
761
812
|
if (direct === null) {
|
|
762
813
|
return this.#parts.mint.within(frame, dimension, action, caller.subject, 'all')
|
|
763
814
|
}
|
|
764
815
|
const held = this.#heldContainers(caller.perms, action, dimension)
|
|
765
|
-
|
|
816
|
+
const inside = this.#heldContained(caller.perms, action, dimension)
|
|
817
|
+
if (direct.length === 0 && held.length === 0 && inside.length === 0) {
|
|
766
818
|
throw new ForbiddenError(`The caller holds '${action}' in no ${dimension}`)
|
|
767
819
|
}
|
|
768
|
-
return this.#parts.mint.within(frame, dimension, action, caller.subject, direct, held)
|
|
820
|
+
return this.#parts.mint.within(frame, dimension, action, caller.subject, direct, held, inside)
|
|
769
821
|
}
|
|
770
822
|
|
|
771
823
|
/** A scope value the caller acts in: without a grant on it or on a container of it, 403 whether it exists or not. */
|
|
@@ -908,7 +960,7 @@ export class ContractRuntime {
|
|
|
908
960
|
admission.repeatableToken,
|
|
909
961
|
)
|
|
910
962
|
}
|
|
911
|
-
return await this.#publicInvocation(served, request, body, fields, query, carried, authority
|
|
963
|
+
return await this.#publicInvocation(served, request, body, fields, query, carried, authority, exec)
|
|
912
964
|
} finally {
|
|
913
965
|
await multipart?.close()
|
|
914
966
|
release()
|
|
@@ -974,9 +1026,10 @@ export class ContractRuntime {
|
|
|
974
1026
|
fields: Readonly<Record<string, unknown>>,
|
|
975
1027
|
query: Readonly<Record<string, unknown>> | undefined,
|
|
976
1028
|
carried: Set<string>,
|
|
977
|
-
perms: readonly PermissionEntry[],
|
|
1029
|
+
authority: { readonly perms: readonly PermissionEntry[]; readonly token: string },
|
|
978
1030
|
exec: RequestExecutionContext | undefined,
|
|
979
1031
|
): Promise<Response | FileDownload> {
|
|
1032
|
+
const { perms } = authority
|
|
980
1033
|
const kicks = kickRecorder(`'${served.row.id}'`, served.site.declaration.kicks)
|
|
981
1034
|
const frame = new RequestFrame(crypto.randomUUID(), { readOnly: served.site.kind === 'query', endpoint: served.row.id })
|
|
982
1035
|
const work = new FileWork(frame, frame.id)
|
|
@@ -995,55 +1048,62 @@ export class ContractRuntime {
|
|
|
995
1048
|
return this.#parts.mint.app(frame, action, subject)
|
|
996
1049
|
}
|
|
997
1050
|
let outcome: 'open' | 'committing' = 'open'
|
|
1051
|
+
let budget: Deadline | null = null
|
|
998
1052
|
try {
|
|
999
1053
|
// The file parts come after the challenge, so a refused visitor stores nothing.
|
|
1000
1054
|
const input = body.multipart === null
|
|
1001
1055
|
? fields
|
|
1002
1056
|
: this.#parts.tokens.verifyFields(served.row.input, await this.#fileInput(served, body.multipart, work), carried)
|
|
1003
1057
|
// The handler's time starts after the last body byte, so a slow upload does not eat it.
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
result
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1058
|
+
budget = handlerSignal(request, served.admission.timeoutMs)
|
|
1059
|
+
const { signal } = budget
|
|
1060
|
+
const answer = await enclosedAnswer(invocation, this.#hold(served.row.id, exec), async () => {
|
|
1061
|
+
let result: unknown
|
|
1062
|
+
try {
|
|
1063
|
+
result = await runInRequest(frame, async () => {
|
|
1064
|
+
const handles = invocation.handles(
|
|
1065
|
+
served.site.kind,
|
|
1066
|
+
served.site.declaration,
|
|
1067
|
+
subject,
|
|
1068
|
+
{ carried },
|
|
1069
|
+
this.#parts.fileKinds?.binding(work),
|
|
1070
|
+
)
|
|
1071
|
+
const where = `'${served.row.id}'`
|
|
1072
|
+
const ctx = Object.freeze({
|
|
1073
|
+
principal: Object.freeze({ kind: 'public', endpoint: served.row.id }),
|
|
1074
|
+
requestId: frame.id,
|
|
1075
|
+
signal,
|
|
1076
|
+
client: requestClient(request, this.#parts.clientVouched),
|
|
1077
|
+
url: this.#parts.url,
|
|
1078
|
+
app,
|
|
1079
|
+
db: Object.freeze({ app: (action: string) => handles.app(app(action)) }),
|
|
1080
|
+
...this.#signing(`'${served.row.id}'`, served.site.declaration.signs),
|
|
1081
|
+
files: storedFilesHandle(this.#parts.fileKinds, work, {
|
|
1082
|
+
where,
|
|
1083
|
+
check: boundTo(frame, () => false, 'ctx.files'),
|
|
1084
|
+
writes: served.site.kind !== 'query',
|
|
1085
|
+
}),
|
|
1086
|
+
...this.#terminals(authority.token, request.headers.get(REQUEST_ID_HEADER) ?? frame.id),
|
|
1087
|
+
kick: kicks.kick,
|
|
1088
|
+
})
|
|
1089
|
+
const args = { input, ctx, ...(query === undefined ? {} : { query }) }
|
|
1090
|
+
return await runAsSite(`'${served.row.id}'`, siteEgress(served.site.declaration.egress, null), async () => await served.handler(args))
|
|
1031
1091
|
})
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
await commitAnswer(invocation, signal, answer)
|
|
1092
|
+
} finally {
|
|
1093
|
+
frame.end()
|
|
1094
|
+
}
|
|
1095
|
+
const output = served.row.output
|
|
1096
|
+
const answer = output !== null && isFileShape(output)
|
|
1097
|
+
? await this.#fileKinds(served.row.id).download(work, served.row.id, output.kind, result)
|
|
1098
|
+
: outputResponse(served.row, served.output, result, request, carried)
|
|
1099
|
+
outcome = 'committing'
|
|
1100
|
+
await commitAnswer(invocation, signal, answer)
|
|
1101
|
+
return answer
|
|
1102
|
+
})
|
|
1044
1103
|
this.#kickAll(kicks.kicked(), exec)
|
|
1045
1104
|
return answer
|
|
1046
1105
|
} finally {
|
|
1106
|
+
budget?.clear()
|
|
1047
1107
|
await invocation.rollback()
|
|
1048
1108
|
if (outcome === 'open') {
|
|
1049
1109
|
await this.#parts.fileKinds?.release(work)
|
|
@@ -1066,7 +1126,7 @@ export class ContractRuntime {
|
|
|
1066
1126
|
): Promise<Response> {
|
|
1067
1127
|
admitContentType(served.row, request)
|
|
1068
1128
|
const limit = maxBytesOf(served.row)
|
|
1069
|
-
const body = await
|
|
1129
|
+
const body = await readBodyInTime(request, limit)
|
|
1070
1130
|
const tables = this.#framework().webhooks
|
|
1071
1131
|
const bindingId = params[WEBHOOK_BINDING_PARAMETER] ?? ''
|
|
1072
1132
|
const binding = BINDING_ID.test(bindingId) ? await tables.binding(served.webhook, bindingId) : null
|
|
@@ -1130,60 +1190,63 @@ export class ContractRuntime {
|
|
|
1130
1190
|
// The claim stays outside the delivery's transaction, and its completion commits inside it, with the work.
|
|
1131
1191
|
const invocation = site === undefined ? null : new Invocation(frame, this.#data(), false)
|
|
1132
1192
|
const kicks = kickRecorder(`webhook '${served.webhook}'`, site?.kicks ?? [])
|
|
1133
|
-
const
|
|
1193
|
+
const budget = handlerSignal(request, budgetMs)
|
|
1194
|
+
const { signal } = budget
|
|
1134
1195
|
let committing = false
|
|
1135
1196
|
try {
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1197
|
+
await enclosed(invocation, this.#hold(served.row.id, exec), async () => {
|
|
1198
|
+
try {
|
|
1199
|
+
await runInRequest(frame, async () => {
|
|
1200
|
+
const subject = webhookSubject(binding.id)
|
|
1201
|
+
const reach = this.#webhookReach(served, frame, subject, binding.scope.value, chain)
|
|
1202
|
+
const principal: WebhookPrincipal = Object.freeze({ kind: 'webhook', bindingId: binding.id })
|
|
1203
|
+
const delivery = Object.freeze({ key })
|
|
1204
|
+
const handles = site === undefined || invocation === null
|
|
1205
|
+
? null
|
|
1206
|
+
: invocation.handles('mutation', site, subject, 'declared', this.#parts.fileKinds?.binding(work))
|
|
1207
|
+
const data = site === undefined || invocation === null || handles === null
|
|
1208
|
+
? {}
|
|
1209
|
+
: {
|
|
1210
|
+
db: siteData(handles, (action) => handles.app(reach.app(action))),
|
|
1211
|
+
sql: site.sql === null ? undeclaredSql(served.row.id, 'mutation') : invocation.sql(site.sql),
|
|
1212
|
+
}
|
|
1213
|
+
const client = requestClient(request, this.#parts.clientVouched)
|
|
1214
|
+
const args = {
|
|
1215
|
+
input,
|
|
1216
|
+
ctx: Object.freeze({
|
|
1217
|
+
principal,
|
|
1218
|
+
requestId: frame.id,
|
|
1219
|
+
signal,
|
|
1220
|
+
client,
|
|
1221
|
+
url: this.#parts.url,
|
|
1222
|
+
...reach.context,
|
|
1223
|
+
delivery,
|
|
1224
|
+
files: storedFilesHandle(this.#parts.fileKinds, work, {
|
|
1225
|
+
where: `webhook '${served.webhook}'`,
|
|
1226
|
+
check: boundTo(frame, () => false, 'ctx.files'),
|
|
1227
|
+
writes: true,
|
|
1228
|
+
}),
|
|
1229
|
+
...this.#signing(`webhook '${served.webhook}'`, site?.signs ?? []),
|
|
1230
|
+
kick: kicks.kick,
|
|
1231
|
+
...data,
|
|
1166
1232
|
}),
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
}
|
|
1185
|
-
committing = true
|
|
1186
|
-
await invocation?.commit(signal)
|
|
1233
|
+
}
|
|
1234
|
+
if (site === undefined) {
|
|
1235
|
+
await served.handler(args)
|
|
1236
|
+
} else {
|
|
1237
|
+
await runAsSite(`webhook '${served.webhook}'`, siteEgress(site.egress, null), async () => await served.handler(args))
|
|
1238
|
+
}
|
|
1239
|
+
})
|
|
1240
|
+
} finally {
|
|
1241
|
+
frame.end()
|
|
1242
|
+
}
|
|
1243
|
+
const using = invocation === null ? undefined : await invocation.frameworkSite('the delivery claim').session()
|
|
1244
|
+
if (!(await tables.completeDelivery(claim, using))) {
|
|
1245
|
+
throw new ServerFault('webhook_claim_lost', `the claim of a delivery to '${served.webhook}' expired and another copy took it over`)
|
|
1246
|
+
}
|
|
1247
|
+
committing = true
|
|
1248
|
+
await invocation?.commit(signal)
|
|
1249
|
+
})
|
|
1187
1250
|
this.#kickAll(kicks.kicked(), exec)
|
|
1188
1251
|
} catch (error) {
|
|
1189
1252
|
await invocation?.rollback()
|
|
@@ -1196,6 +1259,8 @@ export class ContractRuntime {
|
|
|
1196
1259
|
console.error(`fabrika: ${served.row.id}: delivery_failed; the provider's redelivery runs it again`)
|
|
1197
1260
|
await tables.releaseDelivery(claim)
|
|
1198
1261
|
throw error
|
|
1262
|
+
} finally {
|
|
1263
|
+
budget.clear()
|
|
1199
1264
|
}
|
|
1200
1265
|
return jsonResponse({ result: { received: true } }, 200)
|
|
1201
1266
|
}
|
|
@@ -1257,6 +1322,14 @@ export class ContractRuntime {
|
|
|
1257
1322
|
return values.length === 0 ? [] : [{ dimension: container, values }]
|
|
1258
1323
|
})
|
|
1259
1324
|
}
|
|
1325
|
+
|
|
1326
|
+
/** The caller's grants of `action` on every dimension that lies inside `dimension`. */
|
|
1327
|
+
#heldContained(perms: PermissionEntry[], action: string, dimension: string): ContainerGrant[] {
|
|
1328
|
+
return [...this.#parts.containers].flatMap(([inner, containers]) => {
|
|
1329
|
+
const values = containers.includes(dimension) ? scopedValues(perms, action, inner) ?? [] : []
|
|
1330
|
+
return values.length === 0 ? [] : [{ dimension: inner, values }]
|
|
1331
|
+
})
|
|
1332
|
+
}
|
|
1260
1333
|
}
|
|
1261
1334
|
|
|
1262
1335
|
/** `ctx.db` (by the access form) and `ctx.sql` of a staged-kit endpoint: the declared tag, or one that refuses. */
|
|
@@ -1475,7 +1548,7 @@ async function readInput(
|
|
|
1475
1548
|
const limit = served.row.admission?.maxBytes ?? null
|
|
1476
1549
|
const text = limit === null
|
|
1477
1550
|
? await request.text()
|
|
1478
|
-
: new TextDecoder().decode(await
|
|
1551
|
+
: new TextDecoder().decode(await readBodyInTime(request, limit))
|
|
1479
1552
|
if (mediaType(request.headers.get('Content-Type')) === FORM_URLENCODED_TYPE) {
|
|
1480
1553
|
body = paramsObject(new URLSearchParams(text), served.row.input)
|
|
1481
1554
|
} else if (text.trim() !== '') {
|
|
@@ -1547,16 +1620,17 @@ async function openMultipart(
|
|
|
1547
1620
|
if (Number.isFinite(declared) && declared > bodyBytes) {
|
|
1548
1621
|
throw tooLarge()
|
|
1549
1622
|
}
|
|
1550
|
-
const
|
|
1623
|
+
const timer = deadline(bodyDeadlineMs(maxBytes), request.signal)
|
|
1551
1624
|
const body = boundedStream(request.body ?? new Blob([]).stream(), bodyBytes)
|
|
1552
1625
|
const parts = multipartParts(body.stream, boundary, { maxHeaderBytes: PROCEDURE_LIMITS.maxPartHeaderBytes })
|
|
1553
|
-
const failure = (error: unknown) => (body.exceeded() ? tooLarge() : multipartFailure(error, parts,
|
|
1626
|
+
const failure = (error: unknown) => (body.exceeded() ? tooLarge() : multipartFailure(error, parts, timer.signal, request))
|
|
1554
1627
|
const stop = () => {
|
|
1555
1628
|
parts.cancel().catch(() => {})
|
|
1556
1629
|
}
|
|
1557
|
-
|
|
1630
|
+
timer.signal.addEventListener('abort', stop, { once: true })
|
|
1558
1631
|
const close = async () => {
|
|
1559
|
-
|
|
1632
|
+
timer.signal.removeEventListener('abort', stop)
|
|
1633
|
+
timer.clear()
|
|
1560
1634
|
await parts.cancel()
|
|
1561
1635
|
}
|
|
1562
1636
|
let json: Readonly<Record<string, unknown>>
|
|
@@ -1617,6 +1691,36 @@ async function openMultipart(
|
|
|
1617
1691
|
}
|
|
1618
1692
|
}
|
|
1619
1693
|
|
|
1694
|
+
/** The site an invocation's long hold is logged under, and where it is reported. */
|
|
1695
|
+
interface Hold {
|
|
1696
|
+
readonly site: string
|
|
1697
|
+
readonly held: (heldMs: number) => void
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
/** Runs `work` in the invocation's transaction (`Invocation.enclose`), or as is without one. */
|
|
1701
|
+
function enclosed<T>(invocation: Invocation | null, hold: Hold, work: () => Promise<T>): Promise<T> {
|
|
1702
|
+
return invocation === null ? work() : invocation.enclose(work, hold.site, hold.held)
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
/**
|
|
1706
|
+
* An answer's work, from the handler to its commit, in the invocation's transaction. Where the transaction encloses
|
|
1707
|
+
* the work (SQLite) its COMMIT runs after `commitAnswer`, so a failed one cancels a file's object stream here.
|
|
1708
|
+
*/
|
|
1709
|
+
async function enclosedAnswer<A extends Response | FileDownload>(invocation: Invocation | null, hold: Hold, work: () => Promise<A>): Promise<A> {
|
|
1710
|
+
let answer: A | undefined
|
|
1711
|
+
try {
|
|
1712
|
+
return await enclosed(invocation, hold, async () => {
|
|
1713
|
+
answer = await work()
|
|
1714
|
+
return answer
|
|
1715
|
+
})
|
|
1716
|
+
} catch (error) {
|
|
1717
|
+
if (answer instanceof FileDownload) {
|
|
1718
|
+
await answer.body.cancel()
|
|
1719
|
+
}
|
|
1720
|
+
throw error
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1620
1724
|
/**
|
|
1621
1725
|
* Commits the invocation of an answer that is ready: a file's object stream was opened before the commit, so a failed
|
|
1622
1726
|
* commit cancels it and nothing of it is sent.
|