@fabrika/app 0.8.2 → 0.9.0-cloudflare.1
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 +27 -3
- package/skill/references/app-setup.md +8 -8
- package/skill/references/dev.md +2 -1
- package/skill/references/endpoint.md +7 -3
- package/skill/references/errors.md +39 -35
- package/skill/references/files.md +4 -2
- package/skill/references/table-and-migration.md +19 -2
- package/skill/references/tests.md +11 -0
- package/skill/references/webhooks-links-connectors.md +1 -1
- package/src/build-recipe.ts +198 -0
- package/src/build.ts +55 -4
- package/src/bun.ts +23 -8
- package/src/cloudflare/application.ts +352 -0
- package/src/cloudflare/bindings.ts +138 -0
- package/src/cloudflare/names.ts +10 -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 +2 -0
- package/src/contract/build.ts +35 -1
- 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/kit.ts +4 -1
- 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/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 +61 -4
- package/src/implement/bounded.ts +21 -2
- package/src/implement/capability.ts +15 -3
- package/src/implement/dispatch.ts +249 -145
- 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 +8 -0
- 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 +33 -17
- package/src/jobs/schedule.ts +7 -1
- package/src/jobs/scrub.ts +14 -1
- package/src/testing/endpoint-access.ts +180 -137
- 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 +27 -0
- package/types/src/cloudflare/names.d.ts +6 -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 +274 -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 +3 -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 +114 -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 +142 -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 +220 -0
- package/types/src/implement/egress.d.ts +14 -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 +151 -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/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 +107 -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
package/src/cloudflare.ts
CHANGED
|
@@ -1,17 +1,31 @@
|
|
|
1
1
|
// ─── Cloudflare Workers adapter ──────────────────────────────────────
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import {
|
|
4
|
+
type ApplicationObjects,
|
|
5
|
+
FABRIKA_APPLICATION_OBJECT,
|
|
6
|
+
FABRIKA_APPLICATION_OBJECTS,
|
|
7
|
+
FABRIKA_PROXY_SECRET,
|
|
8
|
+
OPS_PREFIX,
|
|
9
|
+
opsRefusal,
|
|
10
|
+
parseApplicationObjects,
|
|
11
|
+
parseOpsOperation,
|
|
12
|
+
PROXY_SECRET_HEADER,
|
|
13
|
+
} from '@fabrika/auth-core'
|
|
14
|
+
import { type ContractWorkerOptions, type ImplementationLoader, registerApplication, TICK_HEADER } from './cloudflare/application.js'
|
|
15
|
+
import { method } from './cloudflare/bindings.js'
|
|
16
|
+
import { CLOUDFLARE_APPLICATION_BINDING } from './cloudflare/names.js'
|
|
17
|
+
import { jsonBody, objectNamed, opsAccess, opsAuthenticated } from './cloudflare/ops.js'
|
|
6
18
|
import type { FetchService, RequestExecutionContext } from './fetch.js'
|
|
7
|
-
import { boot, type ContractApp, type IamKeySource } from './implement/boot.js'
|
|
8
|
-
import { CONNECT_BINDING_PREFIX, type ConnectFetch } from './implement/connect.js'
|
|
9
|
-
import { type CrashEvents, environmentCrashReporter, reportSurvivedCrashes } from './implement/crashes.js'
|
|
10
|
-
import type { SpaAssets } from './implement/dispatch.js'
|
|
11
|
-
import { captureEnvironment } from './implement/environment.js'
|
|
12
|
-
import type { ContractImplementation } from './implement/implement.js'
|
|
13
19
|
|
|
14
20
|
export { markProductionProcess } from '@fabrika/db/production'
|
|
21
|
+
export {
|
|
22
|
+
type ApplicationMigration,
|
|
23
|
+
type ApplicationState,
|
|
24
|
+
type ApplicationStorage,
|
|
25
|
+
type ContractWorkerOptions,
|
|
26
|
+
FabrikaApplication,
|
|
27
|
+
type ImplementationLoader,
|
|
28
|
+
} from './cloudflare/application.js'
|
|
15
29
|
|
|
16
30
|
/** Minimal structural view of a Workers `ExecutionContext`. */
|
|
17
31
|
export interface CloudflareExecutionContext extends RequestExecutionContext {
|
|
@@ -58,166 +72,105 @@ export function createCloudflareWorker<Env>(
|
|
|
58
72
|
|
|
59
73
|
// ─── Contract applications ───────────────────────────────────────────
|
|
60
74
|
|
|
61
|
-
/** The
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return null
|
|
67
|
-
}
|
|
68
|
-
const found: unknown = Reflect.get(target, name)
|
|
69
|
-
return typeof found === 'function' ? (...args) => Reflect.apply(found, target, args) : null
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function bindingKeySource(binding: unknown): IamKeySource {
|
|
73
|
-
const getJwks = method(binding, 'getJwks')
|
|
74
|
-
if (getJwks === null) {
|
|
75
|
-
throw new Error('the IAM binding is missing (env.IAM); the platform binds it to every application Worker')
|
|
76
|
-
}
|
|
77
|
-
return {
|
|
78
|
-
async getJwks() {
|
|
79
|
-
const jwks = parseJwks(await getJwks())
|
|
80
|
-
if (jwks === null) {
|
|
81
|
-
throw new Error('IAM returned no usable key set')
|
|
82
|
-
}
|
|
83
|
-
return jwks
|
|
84
|
-
},
|
|
75
|
+
/** The environment's proxy secret, which every request the Worker forwards to its object carries. */
|
|
76
|
+
function proxySecretOf(env: Readonly<Record<string, unknown>>): string {
|
|
77
|
+
const secret = env[FABRIKA_PROXY_SECRET]
|
|
78
|
+
if (typeof secret !== 'string' || secret === '') {
|
|
79
|
+
throw new Error(`${FABRIKA_PROXY_SECRET} is not set; the application's Worker forwards nothing to its object without it`)
|
|
85
80
|
}
|
|
81
|
+
return secret
|
|
86
82
|
}
|
|
87
83
|
|
|
88
84
|
/**
|
|
89
|
-
*
|
|
90
|
-
*
|
|
85
|
+
* The object of ordinary traffic and the objects an ops request may name, all in the environment's own namespace.
|
|
86
|
+
* Without either variable (an older deploy) the one object is `application`.
|
|
91
87
|
*/
|
|
92
|
-
function
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
88
|
+
function applicationObjects(env: Readonly<Record<string, unknown>>): ApplicationObjects {
|
|
89
|
+
const object = env[FABRIKA_APPLICATION_OBJECT]
|
|
90
|
+
const objects = env[FABRIKA_APPLICATION_OBJECTS]
|
|
91
|
+
const parsed = parseApplicationObjects(typeof object === 'string' ? object : undefined, typeof objects === 'string' ? objects : undefined)
|
|
92
|
+
if (parsed === null) {
|
|
93
|
+
throw new Error(`${FABRIKA_APPLICATION_OBJECT} and ${FABRIKA_APPLICATION_OBJECTS} name no valid set of objects; a deploy writes both`)
|
|
96
94
|
}
|
|
97
|
-
return
|
|
98
|
-
const call = method(binding, name)
|
|
99
|
-
if (call === null) {
|
|
100
|
-
throw new Error(`the IAM binding has no ${name}`)
|
|
101
|
-
}
|
|
102
|
-
return await call(input)
|
|
103
|
-
})
|
|
95
|
+
return parsed
|
|
104
96
|
}
|
|
105
97
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
98
|
+
/** The object `name` of the environment's namespace (`FABRIKA_APPLICATION`), as a fetch. */
|
|
99
|
+
function applicationObject(env: Readonly<Record<string, unknown>>, name: string): (request: Request) => Promise<Response> {
|
|
100
|
+
const namespace = env[CLOUDFLARE_APPLICATION_BINDING]
|
|
101
|
+
const idFromName = method(namespace, 'idFromName')
|
|
102
|
+
const get = method(namespace, 'get')
|
|
103
|
+
if (idFromName === null || get === null) {
|
|
104
|
+
throw new Error(`the ${CLOUDFLARE_APPLICATION_BINDING} binding is missing; the provider binds the application's Durable Object to its Worker`)
|
|
110
105
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
106
|
+
const stub: unknown = get(idFromName(name))
|
|
107
|
+
// Read, not tested with `in`: a stub answers `fetch` without owning the property.
|
|
108
|
+
const fetchObject: unknown = typeof stub === 'object' && stub !== null ? Reflect.get(stub, 'fetch') : undefined
|
|
109
|
+
if (typeof fetchObject !== 'function') {
|
|
110
|
+
throw new Error(`the ${CLOUDFLARE_APPLICATION_BINDING} binding answered no object`)
|
|
111
|
+
}
|
|
112
|
+
return async (request) => {
|
|
113
|
+
const response: unknown = await Reflect.apply(fetchObject, stub, [request])
|
|
114
|
+
if (!(response instanceof Response)) {
|
|
115
|
+
throw new Error("the application's object answered something that is not a Response")
|
|
116
|
+
}
|
|
117
|
+
return response
|
|
119
118
|
}
|
|
120
119
|
}
|
|
121
120
|
|
|
122
121
|
/**
|
|
123
|
-
*
|
|
124
|
-
* the
|
|
125
|
-
* carries the owner's host, which the proxy Worker selects the application by.
|
|
122
|
+
* An ops request reaches the object its body names, when the environment lists it (ADR-0069 §1). Every refusal here
|
|
123
|
+
* is the same answer the object gives, and the object checks everything again.
|
|
126
124
|
*/
|
|
127
|
-
function
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
: undefined
|
|
134
|
-
if (typeof fetchOwner === 'function') {
|
|
135
|
-
bindings[name] = async (request) => {
|
|
136
|
-
const response: unknown = await Reflect.apply(fetchOwner, binding, [request])
|
|
137
|
-
if (!(response instanceof Response)) {
|
|
138
|
-
throw new Error(`the ${name} binding answered something that is not a Response`)
|
|
139
|
-
}
|
|
140
|
-
return response
|
|
141
|
-
}
|
|
142
|
-
}
|
|
125
|
+
async function forwardOps(request: Request, env: Readonly<Record<string, unknown>>): Promise<Response> {
|
|
126
|
+
const access = opsAccess(env)
|
|
127
|
+
// Receiver authentication of Control's key, as the object runs it; no enforcement decision. It runs before the body is
|
|
128
|
+
// read or any object looked up, so an unauthenticated caller wakes no object and learns nothing of the names.
|
|
129
|
+
if (access === null || !opsAuthenticated(request, access)) {
|
|
130
|
+
return opsRefusal()
|
|
143
131
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
export interface ContractWorkerOptions {
|
|
151
|
-
/** Where the isolate hears of an unhandled rejection; the global `unhandledrejection` event by default. */
|
|
152
|
-
readonly rejections?: CrashEvents
|
|
153
|
-
/** What posts a report to Operations' ingest; `fetch` by default. */
|
|
154
|
-
readonly operationsFetch?: (request: Request) => Promise<Response>
|
|
132
|
+
const operation = parseOpsOperation(new URL(request.url).pathname)
|
|
133
|
+
const object = operation === null ? null : objectNamed(operation, await jsonBody(request.clone()))
|
|
134
|
+
if (object === null || !access.objects.objects.includes(object)) {
|
|
135
|
+
return opsRefusal()
|
|
136
|
+
}
|
|
137
|
+
return await applicationObject(env, object)(request)
|
|
155
138
|
}
|
|
156
139
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
listen(listener) {
|
|
161
|
-
const rejection = (event: Event) => listener('unhandled_rejection', Reflect.get(event, 'reason'))
|
|
162
|
-
globalThis.addEventListener('unhandledrejection', rejection)
|
|
163
|
-
return () => globalThis.removeEventListener('unhandledrejection', rejection)
|
|
164
|
-
},
|
|
165
|
-
}
|
|
140
|
+
export interface ContractWorker {
|
|
141
|
+
fetch(request: Request, env: Readonly<Record<string, unknown>>, exec: CloudflareExecutionContext): Promise<Response>
|
|
142
|
+
scheduled(controller: CloudflareScheduledController, env: Readonly<Record<string, unknown>>, exec: CloudflareExecutionContext): Promise<void>
|
|
166
143
|
}
|
|
167
144
|
|
|
168
145
|
/**
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
146
|
+
* The Worker module of a contract application, called only by the generated entrypoint, which also exports
|
|
147
|
+
* `FabrikaApplication`. The application runs in the environment's Durable Object (ADR-0065): `fetch` forwards every
|
|
148
|
+
* request to it with the proxy secret, the proxy's own headers unchanged, and `scheduled` forwards the minute tick.
|
|
149
|
+
* An ops request goes to the object its body names instead (ADR-0069). The object captures the environment, imports
|
|
150
|
+
* the application and boots it.
|
|
173
151
|
*/
|
|
174
|
-
export function createContractWorker(load: ImplementationLoader, options: ContractWorkerOptions = {}): {
|
|
175
|
-
|
|
176
|
-
} {
|
|
177
|
-
const booted = new WeakMap<object, Promise<ContractApp>>()
|
|
178
|
-
let bootFailureReported = false
|
|
179
|
-
let watchingRejections = false
|
|
180
|
-
const bootFor = async (env: Readonly<Record<string, unknown>>, exec: CloudflareExecutionContext): Promise<ContractApp> => {
|
|
181
|
-
const variables = Object.fromEntries(Object.entries(env).filter(([name]) => !FRAMEWORK_BINDINGS.has(name)))
|
|
182
|
-
const environment = captureEnvironment(variables)
|
|
183
|
-
const crashes = environmentCrashReporter(environment, options.operationsFetch)
|
|
184
|
-
let contractApp: ContractApp
|
|
185
|
-
try {
|
|
186
|
-
const { default: app } = await load()
|
|
187
|
-
contractApp = boot(app, {
|
|
188
|
-
environment,
|
|
189
|
-
iam: bindingKeySource(env['IAM']),
|
|
190
|
-
assets: bindingAssets(env['ASSETS']),
|
|
191
|
-
membership: bindingMembership(env['IAM']),
|
|
192
|
-
connectBindings: connectBindings(env),
|
|
193
|
-
// The proxy Worker sets `X-Fabrika-Client-Ip` from `CF-Connecting-IP`, the visitor Cloudflare saw.
|
|
194
|
-
clientAddress: 'vouched',
|
|
195
|
-
...(options.operationsFetch === undefined ? {} : { operationsFetch: options.operationsFetch }),
|
|
196
|
-
})
|
|
197
|
-
} catch (error) {
|
|
198
|
-
// Every request of a failed isolate boots again; one report says it.
|
|
199
|
-
if (crashes !== null && !bootFailureReported) {
|
|
200
|
-
bootFailureReported = true
|
|
201
|
-
exec.waitUntil(crashes.report('boot_failed', error))
|
|
202
|
-
}
|
|
203
|
-
throw error
|
|
204
|
-
}
|
|
205
|
-
if (crashes !== null && !watchingRejections) {
|
|
206
|
-
watchingRejections = true
|
|
207
|
-
reportSurvivedCrashes(options.rejections ?? globalRejectionEvents(), crashes)
|
|
208
|
-
}
|
|
209
|
-
return contractApp
|
|
210
|
-
}
|
|
152
|
+
export function createContractWorker(load: ImplementationLoader, options: ContractWorkerOptions = {}): ContractWorker {
|
|
153
|
+
registerApplication(load, options)
|
|
211
154
|
return {
|
|
212
|
-
async fetch(request, env
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
155
|
+
async fetch(request, env) {
|
|
156
|
+
const headers = new Headers(request.headers)
|
|
157
|
+
headers.delete(TICK_HEADER)
|
|
158
|
+
headers.set(PROXY_SECRET_HEADER, proxySecretOf(env))
|
|
159
|
+
const forwarded = new Request(request, { headers })
|
|
160
|
+
if (new URL(request.url).pathname.startsWith(OPS_PREFIX)) {
|
|
161
|
+
return await forwardOps(forwarded, env)
|
|
162
|
+
}
|
|
163
|
+
return await applicationObject(env, applicationObjects(env).object)(forwarded)
|
|
164
|
+
},
|
|
165
|
+
async scheduled(controller, env) {
|
|
166
|
+
const tick = new Request('https://application.invalid/', {
|
|
167
|
+
method: 'POST',
|
|
168
|
+
headers: { [PROXY_SECRET_HEADER]: proxySecretOf(env), [TICK_HEADER]: String(controller.scheduledTime) },
|
|
169
|
+
})
|
|
170
|
+
const response = await applicationObject(env, applicationObjects(env).object)(tick)
|
|
171
|
+
if (!response.ok) {
|
|
172
|
+
throw new Error(`the application's object answered the minute tick with ${response.status}`)
|
|
219
173
|
}
|
|
220
|
-
return (await contractApp).fetch(request, exec)
|
|
221
174
|
},
|
|
222
175
|
}
|
|
223
176
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// A leaf module: the contract (client side included) and the build recipe both read the id rule, and the contract must
|
|
2
|
+
// not reach the recipe's server-side modules through it.
|
|
3
|
+
|
|
4
|
+
/** The contract's application id rule; an id that holds to it reaches a generated script unquoted. */
|
|
5
|
+
export const CONTRACT_APP_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
type SchemaShape,
|
|
25
25
|
type StringShape,
|
|
26
26
|
type TablePolicyArtifact,
|
|
27
|
+
terminalIdentifyProblem,
|
|
27
28
|
type TupleShape,
|
|
28
29
|
} from '@fabrika/auth-core'
|
|
29
30
|
import { isPolicyOrigin, WORKER_BLOB_SOURCE } from '@fabrika/proxy-core/headers'
|
|
@@ -425,6 +426,7 @@ export const contractArtifactSchema: z.ZodType<ContractArtifact> = z.strictObjec
|
|
|
425
426
|
.optional(),
|
|
426
427
|
members: z.strictObject({ manage: action, list: action }).optional(),
|
|
427
428
|
directory: z.literal('labels').optional(),
|
|
429
|
+
terminal: z.strictObject({ identify: z.string().refine((path) => terminalIdentifyProblem(path) === null, 'not an identification path') }).optional(),
|
|
428
430
|
procedures: record(z.strictObject({
|
|
429
431
|
rate: z.strictObject({
|
|
430
432
|
perClient: bucket,
|
package/src/contract/build.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type CapabilityAccessForm,
|
|
14
14
|
type CapabilityRequirement,
|
|
15
15
|
type ChallengeProvider,
|
|
16
|
+
compileEndpointTable,
|
|
16
17
|
CONTRACT_FORMAT,
|
|
17
18
|
type ContractArtifact,
|
|
18
19
|
type CspExtensions,
|
|
@@ -48,6 +49,7 @@ import {
|
|
|
48
49
|
type MessageKind,
|
|
49
50
|
type MountArtifact,
|
|
50
51
|
type OutputShape,
|
|
52
|
+
planEndpoint,
|
|
51
53
|
type ProcedureArtifact,
|
|
52
54
|
type ProcedureChallenge,
|
|
53
55
|
type RoleArtifact,
|
|
@@ -55,11 +57,14 @@ import {
|
|
|
55
57
|
type ScopeBinding,
|
|
56
58
|
tableActions,
|
|
57
59
|
type TablePolicyArtifact,
|
|
60
|
+
type TerminalArtifact,
|
|
61
|
+
terminalIdentifyProblem,
|
|
58
62
|
type TokenPurposeArtifact,
|
|
59
63
|
type WebhookScheme,
|
|
60
64
|
} from '@fabrika/auth-core'
|
|
61
65
|
import { isPolicyOrigin, MAX_CSP_ORIGINS, WORKER_BLOB_SOURCE } from '@fabrika/proxy-core/headers'
|
|
62
66
|
import { z } from 'zod/v4'
|
|
67
|
+
import { CONTRACT_APP_ID_PATTERN as APP_ID } from './app-id'
|
|
63
68
|
import { contractArtifactSchema } from './artifact-schema'
|
|
64
69
|
import {
|
|
65
70
|
type AccessDeclaration,
|
|
@@ -117,7 +122,6 @@ import { ContractError, Problems } from './problems'
|
|
|
117
122
|
import { TOKEN_MAX_DAYS } from './token'
|
|
118
123
|
import { hmacSchemeSchema, WEBHOOK_SCHEME_ALLOWED, webhookSchemeSchema } from './webhook-scheme'
|
|
119
124
|
|
|
120
|
-
const APP_ID = /^[a-z0-9][a-z0-9-]*$/
|
|
121
125
|
const NAME = /^[A-Za-z][A-Za-z0-9_]*$/
|
|
122
126
|
const DIMENSION = /^[a-z][A-Za-z0-9_]*$/
|
|
123
127
|
const ACTION = /^[A-Za-z][A-Za-z0-9_-]*(?:\.[A-Za-z][A-Za-z0-9_-]*)+$/
|
|
@@ -234,6 +238,7 @@ class ArtifactBuilder {
|
|
|
234
238
|
const displayName = this.#displayName()
|
|
235
239
|
const mail = this.#mail()
|
|
236
240
|
const people = this.#people()
|
|
241
|
+
const terminal = this.#terminal()
|
|
237
242
|
const artifact: ContractArtifact = {
|
|
238
243
|
format: CONTRACT_FORMAT,
|
|
239
244
|
app: contract.app,
|
|
@@ -256,6 +261,7 @@ class ArtifactBuilder {
|
|
|
256
261
|
...(displayName === null ? {} : { displayName }),
|
|
257
262
|
...(mail === null ? {} : { mail }),
|
|
258
263
|
...people,
|
|
264
|
+
...(terminal === null ? {} : { terminal }),
|
|
259
265
|
...(Object.keys(this.#procedureRules).length === 0 ? {} : { procedures: this.#procedureRules }),
|
|
260
266
|
...(Object.keys(tokens).length === 0 ? {} : { tokens }),
|
|
261
267
|
}
|
|
@@ -263,10 +269,38 @@ class ArtifactBuilder {
|
|
|
263
269
|
assertNoAmbiguity(artifact)
|
|
264
270
|
} catch (error) {
|
|
265
271
|
this.#problem('endpoints', error instanceof Error ? error.message : String(error))
|
|
272
|
+
return artifact
|
|
273
|
+
}
|
|
274
|
+
if (terminal !== null) {
|
|
275
|
+
this.#checkIdentifyPage(artifact, terminal)
|
|
266
276
|
}
|
|
267
277
|
return artifact
|
|
268
278
|
}
|
|
269
279
|
|
|
280
|
+
/** The terminal's identification page, when its path is one. */
|
|
281
|
+
#terminal(): TerminalArtifact | null {
|
|
282
|
+
const terminal = this.#contract.terminal
|
|
283
|
+
if (terminal === undefined) return null
|
|
284
|
+
const problem = terminalIdentifyProblem(terminal.identify)
|
|
285
|
+
if (problem !== null) {
|
|
286
|
+
this.#problem('terminal.identify', `'${terminal.identify}' ${problem}`)
|
|
287
|
+
return null
|
|
288
|
+
}
|
|
289
|
+
return { identify: terminal.identify }
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Nobody is signed in on the identification page, so a public SPA mount must serve it: under a credentialed one, the
|
|
294
|
+
* proxy would bounce the terminal's browser back to sign-in, which sends it to the page again.
|
|
295
|
+
*/
|
|
296
|
+
#checkIdentifyPage(artifact: ContractArtifact, terminal: TerminalArtifact): void {
|
|
297
|
+
const plan = planEndpoint(compileEndpointTable(artifact), 'GET', terminal.identify)
|
|
298
|
+
if (plan.outcome !== 'endpoint' || plan.endpoint.kind !== 'public-spa') {
|
|
299
|
+
const served = plan.outcome === 'endpoint' ? `'${plan.endpoint.id}' (${plan.endpoint.kind})` : 'no endpoint'
|
|
300
|
+
this.#problem('terminal.identify', `'${terminal.identify}' is served by ${served}; serve it from an spa mount with access: { public: true }`)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
270
304
|
/** The member directory each endpoint reads, recorded so the approval and the digest cover the reach. */
|
|
271
305
|
#directories(dimensions: ContractArtifact['dimensions']): Record<string, EndpointDirectory> {
|
|
272
306
|
const directories: Record<string, EndpointDirectory> = {}
|
package/src/contract/compile.ts
CHANGED
|
@@ -81,6 +81,7 @@ export function describeContract(contract: AnyContract): ContractDeclaration<Sch
|
|
|
81
81
|
...(vocabulary.mail === undefined ? {} : { mail: vocabulary.mail }),
|
|
82
82
|
...(vocabulary.members === undefined ? {} : { members: vocabulary.members }),
|
|
83
83
|
...(vocabulary.directory === undefined ? {} : { directory: vocabulary.directory }),
|
|
84
|
+
...(vocabulary.terminal === undefined ? {} : { terminal: { identify: vocabulary.terminal.identify } }),
|
|
84
85
|
}
|
|
85
86
|
}
|
|
86
87
|
|
package/src/contract/context.ts
CHANGED
|
@@ -25,6 +25,7 @@ import type { StoredFilesHandle, StoredFilesReadHandle } from '../files/handle'
|
|
|
25
25
|
import type { AppCapability, Ref, Within } from '../implement/capability'
|
|
26
26
|
import type { ApplicationDirectory, LinkIssuing, MemberDirectory, MemberLookup, MemberManagement } from '../implement/membership'
|
|
27
27
|
import type { AppPrincipal, PrincipalOf, PublicPrincipal, ServicePrincipal } from '../implement/principal'
|
|
28
|
+
import type { TerminalIdentification } from '../implement/terminals'
|
|
28
29
|
import type { CapabilityOf, ConnectCall, RequestContext } from '../implement/types'
|
|
29
30
|
import type { WebhookDelivery, WebhookPrincipal } from '../implement/webhook'
|
|
30
31
|
import type { AppMail, JobContext } from '../jobs/types'
|
|
@@ -56,6 +57,8 @@ export interface KitTypes {
|
|
|
56
57
|
readonly dimensions: unknown
|
|
57
58
|
/** The actions of the application's own people, as written; `never` when the vocabulary declares none. */
|
|
58
59
|
readonly members: { readonly manage: string; readonly list: string }
|
|
60
|
+
/** Whether the vocabulary declares `terminal: { identify }`, which gives `ctx.terminals`. */
|
|
61
|
+
readonly terminal: boolean
|
|
59
62
|
readonly index: AnyDbIndex
|
|
60
63
|
/** The table names of `src/db/tables.ts`. */
|
|
61
64
|
readonly tables: string
|
|
@@ -357,6 +360,10 @@ type ConnectContext<K extends KitTypes, Cn extends readonly string[]> = [Cn] ext
|
|
|
357
360
|
? { readonly connect: Unavailable<"declare connect: ['<connector>'] on this endpoint to call another application"> }
|
|
358
361
|
: { readonly connect: { readonly [N in Cn[number]]: ConnectorOf<K['imports'][N]> } }
|
|
359
362
|
|
|
363
|
+
/** `ctx.terminals`, where the vocabulary declares the terminal identification page. */
|
|
364
|
+
type TerminalsContext<K extends KitTypes> = K['terminal'] extends true ? { readonly terminals: TerminalIdentification }
|
|
365
|
+
: { readonly terminals: Unavailable<"the application declares no terminal: terminal: { identify: '/identify' } in the vocabulary"> }
|
|
366
|
+
|
|
360
367
|
/** What the handler of an RPC endpoint receives, for mount callers `Caller`, kind `Kind` and its declaration. */
|
|
361
368
|
export type EndpointContext<
|
|
362
369
|
K extends KitTypes,
|
|
@@ -370,6 +377,7 @@ export type EndpointContext<
|
|
|
370
377
|
& MembersContext<K, Acc>
|
|
371
378
|
& LinksContext<Acc, Of<D, 'issues'>>
|
|
372
379
|
& ConnectContext<K, Of<D, 'connect'>>
|
|
380
|
+
& TerminalsContext<K>
|
|
373
381
|
& SqlContext<Of<D, 'sql'>, K['index']>
|
|
374
382
|
& FilesContext<K['index'], Kind>
|
|
375
383
|
& { readonly db: DataOf<K['index'], EndpointData<K['index'], Kind, Acc, Of<D, 'unscoped'>>> }
|
|
@@ -396,6 +404,7 @@ export type PublicContext<K extends KitTypes, Kind extends DataKind, Acts extend
|
|
|
396
404
|
readonly files: Kind extends 'query' ? StoredFilesReadHandle : StoredFilesHandle
|
|
397
405
|
readonly sql: Unavailable<'a public procedure reaches its actions only: it declares no sql'>
|
|
398
406
|
}
|
|
407
|
+
& TerminalsContext<K>
|
|
399
408
|
& {
|
|
400
409
|
readonly db: DataOf<
|
|
401
410
|
K['index'],
|
|
@@ -26,6 +26,8 @@ export interface ContractDeclaration<S> {
|
|
|
26
26
|
readonly members?: { readonly manage: string; readonly list: string }
|
|
27
27
|
/** `'labels'` only; `buildArtifact` refuses another value, and refuses it without `members`. */
|
|
28
28
|
readonly directory?: string
|
|
29
|
+
/** The identification page of a terminal; `buildArtifact` refuses a path a public SPA mount does not serve. */
|
|
30
|
+
readonly terminal?: { readonly identify: string }
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
/**
|
package/src/contract/define.ts
CHANGED
|
@@ -26,6 +26,11 @@ import type { TokenPurposes } from './token'
|
|
|
26
26
|
|
|
27
27
|
export type Path = `/${string}`
|
|
28
28
|
|
|
29
|
+
/** Whether `value` is written as a `Path`; what `buildArtifact` refuses of one is a separate check. */
|
|
30
|
+
export function isPath(value: string): value is Path {
|
|
31
|
+
return value.startsWith('/')
|
|
32
|
+
}
|
|
33
|
+
|
|
29
34
|
/** A job attempt's time limit: 15 minutes by default, at most a day. A job reads no body, so it has no `maxBytes`. */
|
|
30
35
|
export interface JobLimitsOption {
|
|
31
36
|
readonly timeoutMs?: number
|
|
@@ -92,7 +97,7 @@ export interface RoleDef<A extends string, D extends string, R extends string> {
|
|
|
92
97
|
readonly external?: true
|
|
93
98
|
}
|
|
94
99
|
|
|
95
|
-
export interface Vocabulary<A extends string, D extends string, R extends string> extends Presentation, AppPeople<A
|
|
100
|
+
export interface Vocabulary<A extends string, D extends string, R extends string> extends Presentation, AppPeople<A>, AppTerminal {
|
|
96
101
|
readonly app: string
|
|
97
102
|
readonly dimensions: { readonly [K in D]: DimensionDef<A, D, R> }
|
|
98
103
|
readonly actions: { readonly [K in A]: string }
|
|
@@ -124,6 +129,26 @@ export function peopleOf<A extends string>(vocabulary: AppPeople<A>): AppPeople<
|
|
|
124
129
|
}
|
|
125
130
|
}
|
|
126
131
|
|
|
132
|
+
/** The page that identifies a person at an enrolled terminal (ADR-0067). */
|
|
133
|
+
export interface TerminalDeclaration {
|
|
134
|
+
/** An absolute path on the application's own origin, served by a public SPA mount; IAM appends `?identification=<id>`. */
|
|
135
|
+
readonly identify: Path
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* `terminal: { identify: '/identify' }`: the application identifies a person at an enrolled terminal on that page and
|
|
140
|
+
* asserts who stands there (`ctx.terminals`). IAM sends a terminal's browser there only for an application its
|
|
141
|
+
* administrator lists on the terminal. A reviewed declaration.
|
|
142
|
+
*/
|
|
143
|
+
export interface AppTerminal {
|
|
144
|
+
readonly terminal?: TerminalDeclaration
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** A vocabulary's terminal, copied field by field: it exists only when declared. */
|
|
148
|
+
export function terminalOf(vocabulary: AppTerminal): AppTerminal {
|
|
149
|
+
return vocabulary.terminal === undefined ? {} : { terminal: { identify: vocabulary.terminal.identify } }
|
|
150
|
+
}
|
|
151
|
+
|
|
127
152
|
/** How IAM names the application to a person: on its sign-in, password and invitation pages, and in its e-mails. */
|
|
128
153
|
export interface Presentation {
|
|
129
154
|
/** The language of those pages and e-mails; absent, the installation's default. */
|
|
@@ -535,7 +560,7 @@ export function defineContract<
|
|
|
535
560
|
const Dims extends { readonly [K in D]: DimensionDef<NoInfer<A>, NoInfer<D>, NoInfer<R>> },
|
|
536
561
|
const S extends Surface,
|
|
537
562
|
>(
|
|
538
|
-
vocabulary: Presentation & AppPeople<NoInfer<A>> & {
|
|
563
|
+
vocabulary: Presentation & AppPeople<NoInfer<A>> & AppTerminal & {
|
|
539
564
|
readonly app: string
|
|
540
565
|
readonly dimensions: Dims & { readonly [K in D]: DimensionDef<NoInfer<A>, NoInfer<D>, NoInfer<R>> }
|
|
541
566
|
readonly actions: { readonly [K in A]: K extends `${string}*${string}` ? WildcardMessage : string }
|
|
@@ -551,6 +576,7 @@ export function defineContract<
|
|
|
551
576
|
roles: vocabulary.roles,
|
|
552
577
|
...presentationOf(vocabulary),
|
|
553
578
|
...peopleOf(vocabulary),
|
|
579
|
+
...terminalOf(vocabulary),
|
|
554
580
|
},
|
|
555
581
|
dimensions: vocabulary.dimensions,
|
|
556
582
|
surface: surface(surfaceKit()),
|
package/src/contract/kit.ts
CHANGED
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
type Schema,
|
|
46
46
|
type Surface,
|
|
47
47
|
surfaceKit,
|
|
48
|
+
terminalOf,
|
|
48
49
|
type WebhookMount,
|
|
49
50
|
} from './define'
|
|
50
51
|
import type {
|
|
@@ -73,13 +74,14 @@ type Owned<K extends string> = { readonly [P in K]?: never }
|
|
|
73
74
|
|
|
74
75
|
/** The kit's types, computed once from the vocabulary, the tables' index, the imports and the token purposes. */
|
|
75
76
|
export type KitOf<V, Ix, Imp extends KitImports, Tk extends TokenPurposes = Record<never, never>> = V extends
|
|
76
|
-
AppVocabulary<infer A, infer D, infer R, infer Dims, infer M> ? {
|
|
77
|
+
AppVocabulary<infer A, infer D, infer R, infer Dims, infer M, infer T> ? {
|
|
77
78
|
readonly action: A
|
|
78
79
|
readonly dimension: D
|
|
79
80
|
readonly creatable: CreatableDimension<Dims>
|
|
80
81
|
readonly role: R
|
|
81
82
|
readonly dimensions: Dims
|
|
82
83
|
readonly members: M
|
|
84
|
+
readonly terminal: [T] extends [never] ? false : true
|
|
83
85
|
readonly index: Ix
|
|
84
86
|
readonly tables: Ix extends { readonly tables: infer T } ? keyof T & string : never
|
|
85
87
|
readonly imports: Imp
|
|
@@ -555,6 +557,7 @@ function runtimeApp(vocabulary: AnyAppVocabulary, options: KitOptions | undefine
|
|
|
555
557
|
roles: vocabulary.roles,
|
|
556
558
|
...presentationOf(vocabulary),
|
|
557
559
|
...peopleOf(vocabulary),
|
|
560
|
+
...terminalOf(vocabulary),
|
|
558
561
|
},
|
|
559
562
|
dimensions: vocabulary.dimensions,
|
|
560
563
|
surface: declared,
|
package/src/contract/platform.ts
CHANGED
|
@@ -23,8 +23,8 @@ import {
|
|
|
23
23
|
/**
|
|
24
24
|
* The IAM vocabulary of a contract application, marked `contract` so IAM refuses every wildcard written for it: the
|
|
25
25
|
* dimensions as scopes with their container, creator role and members actions; the actions; the roles, a membership
|
|
26
|
-
* role with its scope and the roles that may assign it; the link mounts; the delegations; the workloads;
|
|
27
|
-
* display name and e-mail wording IAM names the application with. Every artifact
|
|
26
|
+
* role with its scope and the roles that may assign it; the link mounts; the delegations; the workloads; the locale,
|
|
27
|
+
* display name and e-mail wording IAM names the application with; and its terminal identification page. Every artifact
|
|
28
28
|
* dimension is a scope, a file collection's `file:<name>` included, because access to a file is checked there.
|
|
29
29
|
*/
|
|
30
30
|
export function schemaFromContract(artifact: ContractArtifact): AppSchema {
|
|
@@ -46,6 +46,7 @@ export function schemaFromContract(artifact: ContractArtifact): AppSchema {
|
|
|
46
46
|
...(artifact.mail === undefined ? {} : { mail: artifact.mail }),
|
|
47
47
|
...(artifact.members === undefined ? {} : { members: { manage: artifact.members.manage, list: artifact.members.list } }),
|
|
48
48
|
...(artifact.directory === 'labels' ? { directory: 'labels' } : {}),
|
|
49
|
+
...(artifact.terminal === undefined ? {} : { terminal: { identify: artifact.terminal.identify } }),
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
|
|
@@ -2,20 +2,35 @@
|
|
|
2
2
|
// nothing else, so the tables, the kit, the groups and the contract can each be typed by it without a cycle.
|
|
3
3
|
// Browser-safe: plain data.
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
type AppMembers,
|
|
7
|
+
type DimensionDef,
|
|
8
|
+
type Presentation,
|
|
9
|
+
presentationOf,
|
|
10
|
+
type RoleDefs,
|
|
11
|
+
type TerminalDeclaration,
|
|
12
|
+
type Vocabulary,
|
|
13
|
+
type WildcardMessage,
|
|
14
|
+
} from './define'
|
|
6
15
|
|
|
7
16
|
/**
|
|
8
|
-
* The vocabulary as `defineVocabulary` returned it, with its literal names, dimension definitions
|
|
9
|
-
* people (`M`, `never` when it declares none).
|
|
17
|
+
* The vocabulary as `defineVocabulary` returned it, with its literal names, dimension definitions, the actions of its
|
|
18
|
+
* people (`M`, `never` when it declares none) and its terminal (`T`, `never` when it declares none).
|
|
10
19
|
*/
|
|
11
|
-
export interface AppVocabulary<
|
|
12
|
-
extends
|
|
13
|
-
|
|
20
|
+
export interface AppVocabulary<
|
|
21
|
+
A extends string,
|
|
22
|
+
D extends string,
|
|
23
|
+
R extends string,
|
|
24
|
+
Dims,
|
|
25
|
+
M extends AppMembers<A> = never,
|
|
26
|
+
T extends TerminalDeclaration = never,
|
|
27
|
+
> extends Vocabulary<A, D, R> {
|
|
14
28
|
readonly dimensions: Dims & { readonly [K in D]: DimensionDef<A, D, R> }
|
|
15
29
|
readonly members?: M
|
|
30
|
+
readonly terminal?: T
|
|
16
31
|
}
|
|
17
32
|
|
|
18
|
-
export type AnyAppVocabulary = AppVocabulary<string, string, string, unknown, AppMembers<string
|
|
33
|
+
export type AnyAppVocabulary = AppVocabulary<string, string, string, unknown, AppMembers<string>, TerminalDeclaration>
|
|
19
34
|
|
|
20
35
|
/**
|
|
21
36
|
* Declares the dimensions, actions and roles, and how IAM names the application (`locale`, `displayName`). `src/vocabulary.ts` exports it; `defineTables`, `defineApp` and the
|
|
@@ -27,6 +42,7 @@ export function defineVocabulary<
|
|
|
27
42
|
const R extends string,
|
|
28
43
|
const Dims extends { readonly [K in D]: DimensionDef<NoInfer<A>, NoInfer<D>, NoInfer<R>> },
|
|
29
44
|
const M extends AppMembers<NoInfer<A>> = never,
|
|
45
|
+
const T extends TerminalDeclaration = never,
|
|
30
46
|
>(
|
|
31
47
|
vocabulary: Presentation & {
|
|
32
48
|
readonly app: string
|
|
@@ -35,8 +51,9 @@ export function defineVocabulary<
|
|
|
35
51
|
readonly roles: RoleDefs<NoInfer<A>, NoInfer<D>, R>
|
|
36
52
|
readonly members?: M
|
|
37
53
|
readonly directory?: 'labels'
|
|
54
|
+
readonly terminal?: T
|
|
38
55
|
},
|
|
39
|
-
): AppVocabulary<A, D, R, Dims, M> {
|
|
56
|
+
): AppVocabulary<A, D, R, Dims, M, T> {
|
|
40
57
|
const people = vocabulary.members
|
|
41
58
|
return {
|
|
42
59
|
app: vocabulary.app,
|
|
@@ -46,5 +63,6 @@ export function defineVocabulary<
|
|
|
46
63
|
...presentationOf(vocabulary),
|
|
47
64
|
...(people === undefined ? {} : { members: people }),
|
|
48
65
|
...(vocabulary.directory === undefined ? {} : { directory: vocabulary.directory }),
|
|
66
|
+
...(vocabulary.terminal === undefined ? {} : { terminal: vocabulary.terminal }),
|
|
49
67
|
}
|
|
50
68
|
}
|