@fabrika/provider-contract 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 +17 -5
- package/src/app-cli.ts +81 -0
- package/src/conformance.ts +221 -0
- package/src/control.ts +85 -16
- package/src/steps.ts +3 -1
- package/types/src/app-cli.d.ts +46 -0
- package/types/src/authoring.d.ts +27 -0
- package/types/src/conformance.d.ts +22 -0
- package/types/src/control.d.ts +523 -0
- package/types/src/index.d.ts +5 -0
- package/types/src/installation.d.ts +37 -0
- package/types/src/json.d.ts +17 -0
- package/types/src/schema.d.ts +17 -0
- package/types/src/steps.d.ts +23 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fabrika/provider-contract",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0-cloudflare.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -12,18 +12,29 @@
|
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
14
|
"src",
|
|
15
|
-
"!src/**/__tests__/**"
|
|
15
|
+
"!src/**/__tests__/**",
|
|
16
|
+
"types"
|
|
16
17
|
],
|
|
17
18
|
"exports": {
|
|
18
19
|
".": {
|
|
19
20
|
"bun": "./src/index.ts",
|
|
20
|
-
"types": "./src/index.ts",
|
|
21
|
+
"types": "./types/src/index.d.ts",
|
|
21
22
|
"default": "./src/index.ts"
|
|
22
23
|
},
|
|
23
24
|
"./installation": {
|
|
24
25
|
"bun": "./src/installation.ts",
|
|
25
|
-
"types": "./src/installation.ts",
|
|
26
|
+
"types": "./types/src/installation.d.ts",
|
|
26
27
|
"default": "./src/installation.ts"
|
|
28
|
+
},
|
|
29
|
+
"./app-cli": {
|
|
30
|
+
"bun": "./src/app-cli.ts",
|
|
31
|
+
"types": "./types/src/app-cli.d.ts",
|
|
32
|
+
"default": "./src/app-cli.ts"
|
|
33
|
+
},
|
|
34
|
+
"./conformance": {
|
|
35
|
+
"bun": "./src/conformance.ts",
|
|
36
|
+
"types": "./types/src/conformance.d.ts",
|
|
37
|
+
"default": "./src/conformance.ts"
|
|
27
38
|
}
|
|
28
39
|
},
|
|
29
40
|
"scripts": {
|
|
@@ -31,6 +42,7 @@
|
|
|
31
42
|
"test": "bun test"
|
|
32
43
|
},
|
|
33
44
|
"dependencies": {
|
|
34
|
-
"@fabrika/auth-core": "0.
|
|
45
|
+
"@fabrika/auth-core": "0.9.0-cloudflare.1",
|
|
46
|
+
"@fabrika/proxy-core": "0.9.0-cloudflare.1"
|
|
35
47
|
}
|
|
36
48
|
}
|
package/src/app-cli.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// What a provider's `/cli` module offers the `fabrika` command for an application: `app build`, `app deploy`,
|
|
2
|
+
// `app register` and `fabrika namespace`. The command loads the module by the provider's id and never branches on it.
|
|
3
|
+
|
|
4
|
+
import type { ProviderDeploymentNamespace, ProviderNamespacePlan } from './control'
|
|
5
|
+
|
|
6
|
+
/** The runtime a contract application's generated entry is written for (`@fabrika/app/build`'s `EntryRuntime`). */
|
|
7
|
+
export type AppEntryRuntime = 'bun' | 'cloudflare'
|
|
8
|
+
|
|
9
|
+
/** What a namespace command asks of Control. The `fabrika` command holds the Control origin, the credential and the transport. */
|
|
10
|
+
export interface NamespaceControl {
|
|
11
|
+
create(namespace: ProviderDeploymentNamespace): Promise<unknown>
|
|
12
|
+
adopt(namespace: ProviderDeploymentNamespace): Promise<unknown>
|
|
13
|
+
reconcile(namespaceId: string): Promise<unknown>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** A plan, or the namespace Control answered with; the `fabrika` command validates and prints either. */
|
|
17
|
+
export type NamespaceCommandResult = { readonly plan: ProviderNamespacePlan } | { readonly namespace: unknown }
|
|
18
|
+
|
|
19
|
+
export interface NamespaceCommandContext {
|
|
20
|
+
readonly source: Readonly<Record<string, string | undefined>>
|
|
21
|
+
/** Reads the Control origin and credential, and throws when either is missing: call it after every other input is checked. */
|
|
22
|
+
control(): NamespaceControl
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface NamespaceCli {
|
|
26
|
+
readonly usage: string
|
|
27
|
+
/** `argv` is the command's own arguments, without the router's `--provider` and `--json`. */
|
|
28
|
+
run(subcommand: string, argv: readonly string[], context: NamespaceCommandContext): Promise<NamespaceCommandResult>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface AppCli {
|
|
32
|
+
readonly provider: string
|
|
33
|
+
readonly entryRuntime: AppEntryRuntime
|
|
34
|
+
/** `fabrika app build`, after the command wrote a contract application's entry; `contract` says whether it did. */
|
|
35
|
+
build?(argv: readonly string[], context: { readonly contract: boolean }): Promise<void>
|
|
36
|
+
deploy?(argv: readonly string[]): Promise<void>
|
|
37
|
+
/** The text `fabrika app register` hands Control: the application's manifest for one environment. */
|
|
38
|
+
registrationManifest?(options: { readonly config: string; readonly env: string }): Promise<string>
|
|
39
|
+
readonly namespaces?: NamespaceCli
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface AppCliModule {
|
|
43
|
+
readonly appCli: AppCli
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const optionalFunction = (value: object, key: string): boolean => {
|
|
47
|
+
const member: unknown = Reflect.get(value, key)
|
|
48
|
+
return member === undefined || typeof member === 'function'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const isNamespaceCli = (value: unknown): value is NamespaceCli =>
|
|
52
|
+
typeof value === 'object'
|
|
53
|
+
&& value !== null
|
|
54
|
+
&& 'usage' in value
|
|
55
|
+
&& typeof value.usage === 'string'
|
|
56
|
+
&& 'run' in value
|
|
57
|
+
&& typeof value.run === 'function'
|
|
58
|
+
|
|
59
|
+
export const isAppCli = (value: unknown): value is AppCli =>
|
|
60
|
+
typeof value === 'object'
|
|
61
|
+
&& value !== null
|
|
62
|
+
&& 'provider' in value
|
|
63
|
+
&& typeof value.provider === 'string'
|
|
64
|
+
&& value.provider.trim() !== ''
|
|
65
|
+
&& 'entryRuntime' in value
|
|
66
|
+
&& (value.entryRuntime === 'bun' || value.entryRuntime === 'cloudflare')
|
|
67
|
+
&& optionalFunction(value, 'build')
|
|
68
|
+
&& optionalFunction(value, 'deploy')
|
|
69
|
+
&& optionalFunction(value, 'registrationManifest')
|
|
70
|
+
&& (!('namespaces' in value) || value.namespaces === undefined || isNamespaceCli(value.namespaces))
|
|
71
|
+
|
|
72
|
+
/** `specifier` names the module in the error, so an operator knows which package to upgrade. */
|
|
73
|
+
export const appCliFromModule = (value: unknown, expectedProvider: string, specifier: string): AppCli => {
|
|
74
|
+
if (typeof value !== 'object' || value === null || !('appCli' in value) || !isAppCli(value.appCli)) {
|
|
75
|
+
throw new Error(`${specifier} must export a valid \`appCli\`; a provider release older than this CLI has none`)
|
|
76
|
+
}
|
|
77
|
+
if (value.appCli.provider !== expectedProvider) {
|
|
78
|
+
throw new Error(`${specifier} provides "${value.appCli.provider}", expected "${expectedProvider}"`)
|
|
79
|
+
}
|
|
80
|
+
return value.appCli
|
|
81
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// The cases every `ControlProvider` passes, whatever cloud it deploys to. A provider's own test file calls
|
|
2
|
+
// `describeControlProviderConformance` once with a harness over its fake cloud: the harness builds the provider and a
|
|
3
|
+
// deploy its cloud accepts, and reports what the cloud was asked to change and to stop.
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from 'bun:test'
|
|
6
|
+
import {
|
|
7
|
+
type ControlProvider,
|
|
8
|
+
type ProviderDeployInput,
|
|
9
|
+
type ProviderDeployOutcome,
|
|
10
|
+
ProviderInvocationAbort,
|
|
11
|
+
type ProviderRunEvents,
|
|
12
|
+
} from './control'
|
|
13
|
+
import type { JsonValue } from './json'
|
|
14
|
+
|
|
15
|
+
/** What the suite hands a fake cloud: `accepted` is called once the cloud has taken the deploy's operation. */
|
|
16
|
+
export interface ConformanceCloudHooks {
|
|
17
|
+
accepted(): void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** One provider over one fresh fake cloud. */
|
|
21
|
+
export interface ConformanceCloud {
|
|
22
|
+
readonly provider: ControlProvider
|
|
23
|
+
/** A deploy the fake cloud accepts and lets succeed. Each case sets `dryRun`, `signal` and `events` itself. */
|
|
24
|
+
readonly input: Omit<ProviderDeployInput, 'dryRun' | 'signal' | 'events'>
|
|
25
|
+
/** The credentials the provider holds itself (a cloud token, a signed URL); no event may carry one. */
|
|
26
|
+
readonly credentials: readonly string[]
|
|
27
|
+
/** Every request that changed the cloud: a write, a created resource, started work. A preview the cloud runs is none. */
|
|
28
|
+
mutations(): readonly string[]
|
|
29
|
+
/** Every request to stop or remove the deploy's work: a cancelled build or job, a deleted version. */
|
|
30
|
+
stops(): readonly string[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ControlProviderConformanceHarness {
|
|
34
|
+
/** Called once per case, and once per event in the lease-loss sweep. */
|
|
35
|
+
create(hooks: ConformanceCloudHooks): ConformanceCloud
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A secret the suite adds to every deploy; no event may carry it. */
|
|
39
|
+
const SECRET_NAME = 'CONFORMANCE_SECRET'
|
|
40
|
+
const SECRET_VALUE = 'conformance-secret-value-7f3a9c'
|
|
41
|
+
|
|
42
|
+
/** What a deploy reported through its events, as Control would have persisted it. */
|
|
43
|
+
interface Recorded {
|
|
44
|
+
readonly logs: string[]
|
|
45
|
+
readonly externalIds: Array<{ readonly id: string; readonly state?: JsonValue }>
|
|
46
|
+
readonly checkpoints: JsonValue[]
|
|
47
|
+
/** Events that hold the lease: `boundary`, `externalId` and `checkpoint`, in order. */
|
|
48
|
+
events: number
|
|
49
|
+
/** A state Control could not store as given; collected, because a provider may swallow what an event throws. */
|
|
50
|
+
readonly violations: string[]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface Invocation {
|
|
54
|
+
readonly input: ProviderDeployInput
|
|
55
|
+
readonly recorded: Recorded
|
|
56
|
+
/** Lose the lease as Control does: the signal aborts, and every later event rejects with the same reason. */
|
|
57
|
+
loseLease(): void
|
|
58
|
+
readonly signal: AbortSignal
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const LEASE_LOST = new ProviderInvocationAbort('lease_lost')
|
|
62
|
+
|
|
63
|
+
/** `lostAt`: the lease goes at this event (1-based), which rejects like every one after it. */
|
|
64
|
+
function invocation(cloud: ConformanceCloud, dryRun: boolean, options: { readonly lostAt?: number } = {}): Invocation {
|
|
65
|
+
const controller = new AbortController()
|
|
66
|
+
const recorded: Recorded = { logs: [], externalIds: [], checkpoints: [], events: 0, violations: [] }
|
|
67
|
+
const loseLease = (): void => {
|
|
68
|
+
if (!controller.signal.aborted) controller.abort(LEASE_LOST)
|
|
69
|
+
}
|
|
70
|
+
const held = async (): Promise<void> => {
|
|
71
|
+
recorded.events += 1
|
|
72
|
+
if (recorded.events === options.lostAt) loseLease()
|
|
73
|
+
if (controller.signal.aborted) throw controller.signal.reason
|
|
74
|
+
}
|
|
75
|
+
const stored = (kind: string, state: JsonValue): JsonValue => {
|
|
76
|
+
const parsed: JsonValue = JSON.parse(JSON.stringify(state))
|
|
77
|
+
if (!Bun.deepEquals(parsed, state, true)) recorded.violations.push(`${kind} state is not JSON as given`)
|
|
78
|
+
return parsed
|
|
79
|
+
}
|
|
80
|
+
const events: ProviderRunEvents = {
|
|
81
|
+
log: (line) => {
|
|
82
|
+
recorded.logs.push(line)
|
|
83
|
+
},
|
|
84
|
+
externalId: async (id, state) => {
|
|
85
|
+
await held()
|
|
86
|
+
recorded.externalIds.push(state === undefined ? { id } : { id, state: stored('externalId', state) })
|
|
87
|
+
},
|
|
88
|
+
checkpoint: async (state) => {
|
|
89
|
+
await held()
|
|
90
|
+
recorded.checkpoints.push(stored('checkpoint', state))
|
|
91
|
+
},
|
|
92
|
+
boundary: held,
|
|
93
|
+
}
|
|
94
|
+
const input: ProviderDeployInput = {
|
|
95
|
+
...cloud.input,
|
|
96
|
+
secrets: { ...cloud.input.secrets, [SECRET_NAME]: SECRET_VALUE },
|
|
97
|
+
dryRun,
|
|
98
|
+
signal: controller.signal,
|
|
99
|
+
events,
|
|
100
|
+
}
|
|
101
|
+
return { input, recorded, loseLease, signal: controller.signal }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The deploy's outcome, or the reason it rejected with. A step a provider stops waiting for may still run, so the
|
|
106
|
+
* cloud is read only after the tasks already queued have run.
|
|
107
|
+
*/
|
|
108
|
+
async function settle(provider: ControlProvider, input: ProviderDeployInput): Promise<ProviderDeployOutcome | { readonly rejected: unknown }> {
|
|
109
|
+
let outcome: ProviderDeployOutcome | { readonly rejected: unknown }
|
|
110
|
+
try {
|
|
111
|
+
outcome = await provider.deploy(input)
|
|
112
|
+
} catch (error) {
|
|
113
|
+
outcome = { rejected: error }
|
|
114
|
+
}
|
|
115
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
116
|
+
return outcome
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** No event carried a credential: the deploy's secrets, its credential managed entries, the upload bearer, the provider's own. */
|
|
120
|
+
function expectCredentialFree(cloud: ConformanceCloud, run: Invocation): void {
|
|
121
|
+
const { input } = run
|
|
122
|
+
const managed = Object.values(input.managedEnvironment).flatMap((entry) => entry.credential && entry.value !== null ? [entry.value] : [])
|
|
123
|
+
const bearer = input.artifactUpload === undefined ? [] : [input.artifactUpload.bearer]
|
|
124
|
+
const reported = JSON.stringify({ logs: run.recorded.logs, externalIds: run.recorded.externalIds, checkpoints: run.recorded.checkpoints })
|
|
125
|
+
for (const credential of [...Object.values(input.secrets), ...managed, ...bearer, ...cloud.credentials]) {
|
|
126
|
+
expect(reported).not.toContain(credential)
|
|
127
|
+
}
|
|
128
|
+
expect(run.recorded.violations).toEqual([])
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function describeControlProviderConformance(name: string, harness: ControlProviderConformanceHarness): void {
|
|
132
|
+
const quiet = (): ConformanceCloud => harness.create({ accepted: () => {} })
|
|
133
|
+
|
|
134
|
+
describe(`${name} conforms to ControlProvider`, () => {
|
|
135
|
+
test('a deploy succeeds, and nothing it reports carries a credential', async () => {
|
|
136
|
+
const cloud = quiet()
|
|
137
|
+
const run = invocation(cloud, false)
|
|
138
|
+
expect(await settle(cloud.provider, run.input)).toMatchObject({ state: 'succeeded' })
|
|
139
|
+
expect(cloud.mutations().length).toBeGreaterThan(0)
|
|
140
|
+
expect(cloud.stops()).toEqual([])
|
|
141
|
+
expectCredentialFree(cloud, run)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
test('a dry run succeeds, changes nothing in the cloud, and reports no credential', async () => {
|
|
145
|
+
const cloud = quiet()
|
|
146
|
+
const run = invocation(cloud, true)
|
|
147
|
+
expect(await settle(cloud.provider, run.input)).toMatchObject({ state: 'succeeded' })
|
|
148
|
+
expect(cloud.mutations()).toEqual([])
|
|
149
|
+
expect(cloud.stops()).toEqual([])
|
|
150
|
+
expectCredentialFree(cloud, run)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test('an invocation whose lease is gone at its first event changes nothing in the cloud', async () => {
|
|
154
|
+
const cloud = quiet()
|
|
155
|
+
const run = invocation(cloud, false, { lostAt: 1 })
|
|
156
|
+
expect(await settle(cloud.provider, run.input)).not.toMatchObject({ state: 'succeeded' })
|
|
157
|
+
expect(cloud.mutations()).toEqual([])
|
|
158
|
+
expect(cloud.stops()).toEqual([])
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
test('a lease lost at any event of a deploy stops nothing, and the deploy claims no success', async () => {
|
|
162
|
+
const cloud = quiet()
|
|
163
|
+
const complete = invocation(cloud, false)
|
|
164
|
+
await settle(cloud.provider, complete.input)
|
|
165
|
+
expect(complete.recorded.events).toBeGreaterThan(0)
|
|
166
|
+
for (let lostAt = 1; lostAt <= complete.recorded.events; lostAt += 1) {
|
|
167
|
+
const lost = quiet()
|
|
168
|
+
const run = invocation(lost, false, { lostAt })
|
|
169
|
+
const outcome = await settle(lost.provider, run.input)
|
|
170
|
+
// The cloud may still finish the work; this invocation may not claim it, and may not stop it.
|
|
171
|
+
expect({ lostAt, outcome }).not.toMatchObject({ outcome: { state: 'succeeded' } })
|
|
172
|
+
expect({ lostAt, stops: lost.stops() }).toEqual({ lostAt, stops: [] })
|
|
173
|
+
expectCredentialFree(lost, run)
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
test('a lease lost once the cloud accepted the deploy stops nothing, in the deploy, a reconcile or a cancel', async () => {
|
|
178
|
+
let run: Invocation | undefined
|
|
179
|
+
let accepted = false
|
|
180
|
+
const cloud = harness.create({
|
|
181
|
+
accepted: () => {
|
|
182
|
+
accepted = true
|
|
183
|
+
run?.loseLease()
|
|
184
|
+
},
|
|
185
|
+
})
|
|
186
|
+
run = invocation(cloud, false)
|
|
187
|
+
const outcome = await settle(cloud.provider, run.input)
|
|
188
|
+
expect(accepted).toBe(true)
|
|
189
|
+
expect(outcome).not.toMatchObject({ state: 'succeeded' })
|
|
190
|
+
expect(cloud.stops()).toEqual([])
|
|
191
|
+
|
|
192
|
+
// Control reconciles and cancels a run by what its last owner recorded.
|
|
193
|
+
const recordedId = run.recorded.externalIds[0]
|
|
194
|
+
if (recordedId === undefined) throw new Error('the deploy recorded no external id before its operation was accepted')
|
|
195
|
+
const providerState = run.recorded.checkpoints.at(-1) ?? recordedId.state
|
|
196
|
+
const reference = {
|
|
197
|
+
runId: run.input.runId,
|
|
198
|
+
externalId: recordedId.id,
|
|
199
|
+
environment: run.input.environment,
|
|
200
|
+
signal: run.signal,
|
|
201
|
+
...(providerState === undefined ? {} : { providerState }),
|
|
202
|
+
}
|
|
203
|
+
const mutations = cloud.mutations().length
|
|
204
|
+
await cloud.provider.reconcile?.(reference).catch(() => {})
|
|
205
|
+
await cloud.provider.cancel?.(reference).catch(() => {})
|
|
206
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
207
|
+
expect(cloud.stops()).toEqual([])
|
|
208
|
+
expect(cloud.mutations()).toHaveLength(mutations)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
test('a registration normalizes to itself, and its envelopes survive JSON', () => {
|
|
212
|
+
const cloud = quiet()
|
|
213
|
+
const normalize = cloud.provider.normalizeRegistration
|
|
214
|
+
if (normalize === undefined) return
|
|
215
|
+
const once = normalize({ app: cloud.input.app, environment: cloud.input.environment })
|
|
216
|
+
expect(normalize(once)).toEqual(once)
|
|
217
|
+
expect(JSON.parse(JSON.stringify(once.environment.target))).toEqual(once.environment.target)
|
|
218
|
+
expect(JSON.parse(JSON.stringify(once.environment.artifact))).toEqual(once.environment.artifact)
|
|
219
|
+
})
|
|
220
|
+
})
|
|
221
|
+
}
|
package/src/control.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { AppSchema, ContractArtifact } from '@fabrika/auth-core'
|
|
1
|
+
import type { AppSchema, ContractArtifact, ManagedEnvironment } from '@fabrika/auth-core'
|
|
2
|
+
import type { InstallationBrand } from '@fabrika/proxy-core/brand'
|
|
2
3
|
import type { JsonValue, ProviderEnvelope } from './json'
|
|
3
4
|
|
|
4
5
|
/** Events whose persistence and presentation belong to the control plane. */
|
|
@@ -15,8 +16,11 @@ export interface ProviderRunEvents {
|
|
|
15
16
|
boundary(): Promise<void>
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
/**
|
|
19
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Complete platform-owned runtime state, parsed by Control (`parseManagedEnvironment`). A provider writes each entry as
|
|
21
|
+
* a secret when it is a `credential`, removes one whose value is null, and removes the stale ones `staleManagedVariables` names.
|
|
22
|
+
*/
|
|
23
|
+
export type ProviderManagedEnvironment = ManagedEnvironment
|
|
20
24
|
|
|
21
25
|
/** Provider-neutral coordinates for obtaining one app revision. */
|
|
22
26
|
export interface ProviderSource {
|
|
@@ -41,8 +45,7 @@ export interface ProviderDeploymentNamespace {
|
|
|
41
45
|
readonly target: ProviderEnvelope
|
|
42
46
|
}
|
|
43
47
|
|
|
44
|
-
|
|
45
|
-
export const FRAMEWORK_RELEASE_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/
|
|
48
|
+
export { FRAMEWORK_RELEASE_PATTERN } from '@fabrika/auth-core'
|
|
46
49
|
|
|
47
50
|
/**
|
|
48
51
|
* The access Control approved for one application environment (ADR-0050): the contract the proxy enforces and the
|
|
@@ -66,10 +69,18 @@ export interface ProviderApprovedAccess {
|
|
|
66
69
|
readonly checker: string
|
|
67
70
|
}
|
|
68
71
|
|
|
72
|
+
/**
|
|
73
|
+
* `dev`: the environment follows a workspace through a platform-owned launcher, which its provider starts instead of the
|
|
74
|
+
* serving entry (ADR-0043). The kind is Control's registry, never the application's.
|
|
75
|
+
*/
|
|
76
|
+
export type ProviderEnvironmentKind = 'standard' | 'dev'
|
|
77
|
+
|
|
69
78
|
/** Persisted deploy data for one app environment. */
|
|
70
79
|
export interface ProviderEnvironment {
|
|
71
80
|
readonly appId: string
|
|
72
81
|
readonly env: string
|
|
82
|
+
/** Absent means `standard`. */
|
|
83
|
+
readonly kind?: ProviderEnvironmentKind
|
|
73
84
|
readonly domain?: string
|
|
74
85
|
/** Canonical externally reachable HTTP(S) origin; independent of provider domain routing. */
|
|
75
86
|
readonly publicOrigin?: string
|
|
@@ -79,12 +90,18 @@ export interface ProviderEnvironment {
|
|
|
79
90
|
* application's proxy manifest entry as `crossOrigins`.
|
|
80
91
|
*/
|
|
81
92
|
readonly crossOrigins?: readonly string[]
|
|
93
|
+
/**
|
|
94
|
+
* The origin of the environment's active Operations ingest DSN, never the DSN: a contract application's proxy admits
|
|
95
|
+
* it in `connect-src`. Control sets it on a deploy; absent while the environment has no active ingest.
|
|
96
|
+
*/
|
|
97
|
+
readonly operationsOrigin?: string
|
|
82
98
|
readonly namespace?: ProviderDeploymentNamespace
|
|
83
99
|
readonly target: ProviderEnvelope
|
|
84
100
|
readonly artifact: ProviderEnvelope
|
|
85
101
|
/**
|
|
86
102
|
* The environment's approved contract, from Control's `approvedSnapshot`. Absent when nothing is approved: then a
|
|
87
103
|
* provider publishes no endpoint table and reconciles no IAM schema, whatever the artifact claims (ADR-0050).
|
|
104
|
+
* Control refuses a non-dry deploy without it.
|
|
88
105
|
*/
|
|
89
106
|
readonly access?: ProviderApprovedAccess
|
|
90
107
|
}
|
|
@@ -137,12 +154,6 @@ export interface ProviderDeployInput {
|
|
|
137
154
|
readonly vars: Readonly<Record<string, string>>
|
|
138
155
|
/** Platform-owned runtime configuration, separate from operator-managed application vars. */
|
|
139
156
|
readonly managedEnvironment: ProviderManagedEnvironment
|
|
140
|
-
/**
|
|
141
|
-
* Every origin IAM may hand this app a session at (ADR-0022). App-wide, not per-environment:
|
|
142
|
-
* IAM's registry is keyed by app id, so the set is assembled from every environment of the app.
|
|
143
|
-
* Absent when the app has no public origin at all, which leaves IAM's registry untouched.
|
|
144
|
-
*/
|
|
145
|
-
readonly returnOrigins?: readonly string[]
|
|
146
157
|
readonly dryRun: boolean
|
|
147
158
|
readonly sourceBinding?: ProviderSourceBinding
|
|
148
159
|
/** Optional short-lived, run-scoped Operations artifact destination. */
|
|
@@ -159,7 +170,10 @@ export interface ProviderDeployInput {
|
|
|
159
170
|
readonly events: ProviderRunEvents
|
|
160
171
|
}
|
|
161
172
|
|
|
162
|
-
/**
|
|
173
|
+
/**
|
|
174
|
+
* A terminal provider operation result. `succeeded` means the version is live: Control then writes the environment's
|
|
175
|
+
* approved IAM schema itself (ADR-0011), so a provider never calls IAM for an application.
|
|
176
|
+
*/
|
|
163
177
|
export interface ProviderTerminalOutcome {
|
|
164
178
|
readonly state: 'succeeded' | 'failed'
|
|
165
179
|
readonly exitCode?: number
|
|
@@ -222,14 +236,10 @@ export interface ProviderRunReference {
|
|
|
222
236
|
|
|
223
237
|
export interface ProviderCancelInput extends ProviderRunReference {
|
|
224
238
|
readonly signal: AbortSignal
|
|
225
|
-
/** See `ProviderDeployInput.returnOrigins`; a cancel that settles an activated deploy projects the same set. */
|
|
226
|
-
readonly returnOrigins?: readonly string[]
|
|
227
239
|
}
|
|
228
240
|
|
|
229
241
|
export interface ProviderReconcileInput extends ProviderRunReference {
|
|
230
242
|
readonly signal: AbortSignal
|
|
231
|
-
/** See `ProviderDeployInput.returnOrigins`; a provider that settles a deploy here projects the same set. */
|
|
232
|
-
readonly returnOrigins?: readonly string[]
|
|
233
243
|
/** Append a line to the run's log. */
|
|
234
244
|
log?(line: string): Promise<void>
|
|
235
245
|
}
|
|
@@ -330,6 +340,11 @@ export interface ProviderNamespaceMutationInput {
|
|
|
330
340
|
readonly namespace: ProviderDeploymentNamespace
|
|
331
341
|
/** Every app environment assigned to the namespace, read under its lease. */
|
|
332
342
|
readonly members: readonly ProviderNamespaceMember[]
|
|
343
|
+
/**
|
|
344
|
+
* The installation's client logo for the pages the namespace's proxy answers with (ADR-0066), read from IAM for this
|
|
345
|
+
* mutation; absent or null shows the fabrika mark. A provider passes it only to a proxy that can parse it.
|
|
346
|
+
*/
|
|
347
|
+
readonly brand?: InstallationBrand | null
|
|
333
348
|
readonly signal: AbortSignal
|
|
334
349
|
readonly events: ProviderNamespaceEvents
|
|
335
350
|
}
|
|
@@ -462,6 +477,19 @@ export interface ProviderNamespaceOperator {
|
|
|
462
477
|
present(namespace: ProviderDeploymentNamespace): ProviderNamespacePresentation
|
|
463
478
|
}
|
|
464
479
|
|
|
480
|
+
/** The app-owned claims of one namespace whose resources Control is asked to confirm gone. */
|
|
481
|
+
export interface ProviderClaimedResourcesInput {
|
|
482
|
+
readonly namespace: ProviderDeploymentNamespace
|
|
483
|
+
readonly resourceKeys: readonly string[]
|
|
484
|
+
readonly signal: AbortSignal
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** What the provider observed for one claimed resource. */
|
|
488
|
+
export interface ProviderClaimedResource {
|
|
489
|
+
readonly resourceKey: string
|
|
490
|
+
readonly exists: boolean
|
|
491
|
+
}
|
|
492
|
+
|
|
465
493
|
/** Optional placement lifecycle owned by providers that use deployment namespaces. */
|
|
466
494
|
export interface ProviderNamespaceCapabilities {
|
|
467
495
|
normalize(namespace: ProviderDeploymentNamespace): ProviderDeploymentNamespace
|
|
@@ -478,11 +506,36 @@ export interface ProviderNamespaceCapabilities {
|
|
|
478
506
|
reconcile(input: ProviderNamespaceMutationInput): Promise<ProviderDeploymentNamespace>
|
|
479
507
|
/** Absent means the namespace has no entry point that follows its members. */
|
|
480
508
|
republish?(input: ProviderNamespaceRepublishInput): Promise<ProviderNamespaceRepublishResult>
|
|
509
|
+
/**
|
|
510
|
+
* Read-only: whether each claimed resource still exists, one answer per key. Throws when it cannot tell, so Control
|
|
511
|
+
* releases a claim only over a resource the provider confirmed gone. Absent means no claim can be released.
|
|
512
|
+
*/
|
|
513
|
+
claimedResources?(input: ProviderClaimedResourcesInput): Promise<readonly ProviderClaimedResource[]>
|
|
481
514
|
/** Absent means the provider changes no namespace in place. */
|
|
482
515
|
readonly updates?: ProviderNamespaceUpdates
|
|
483
516
|
readonly operator?: ProviderNamespaceOperator
|
|
484
517
|
}
|
|
485
518
|
|
|
519
|
+
export interface ProviderAuthoring {
|
|
520
|
+
/** The file NAMES (no `/`) that configure the provider's deploy; a workspace changes none of them, at any depth. */
|
|
521
|
+
readonly protectedFiles: readonly string[]
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** One call on the service of a `dev` environment, between its bootstrap runs. */
|
|
525
|
+
export interface ProviderDevEnvironmentInput {
|
|
526
|
+
readonly environment: ProviderEnvironment
|
|
527
|
+
readonly signal: AbortSignal
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* The service of a `dev` environment outside a run (ADR-0049): Control stops it after an idle period and starts it on
|
|
532
|
+
* the next commit. Files the launcher wrote survive both.
|
|
533
|
+
*/
|
|
534
|
+
export interface ProviderDevEnvironments {
|
|
535
|
+
stop(input: ProviderDevEnvironmentInput): Promise<void>
|
|
536
|
+
start(input: ProviderDevEnvironmentInput): Promise<void>
|
|
537
|
+
}
|
|
538
|
+
|
|
486
539
|
/**
|
|
487
540
|
* The control-plane capability bundle selected statically by one composition root.
|
|
488
541
|
*
|
|
@@ -504,6 +557,22 @@ export interface ControlProvider {
|
|
|
504
557
|
reconcile?(input: ProviderReconcileInput): Promise<ProviderReconcileOutcome>
|
|
505
558
|
readonly secrets?: ProviderManagedSecrets
|
|
506
559
|
readonly namespaces?: ProviderNamespaceCapabilities
|
|
560
|
+
/** MCP authoring (ADR-0049): an environment of this provider can be an authoring target. Absent means none can. */
|
|
561
|
+
readonly authoring?: ProviderAuthoring
|
|
562
|
+
/**
|
|
563
|
+
* Every application environment gets `FABRIKA_PROXY_SECRET`, with or without a namespace proxy: the provider's own
|
|
564
|
+
* hop to the application carries it (Cloudflare's Worker to its Durable Object, ADR-0065). Absent means only a
|
|
565
|
+
* namespace proxy's environments get one.
|
|
566
|
+
*/
|
|
567
|
+
readonly applicationHopSecret?: true
|
|
568
|
+
/** Absent means the provider runs no `dev` environment, and Control refuses one. */
|
|
569
|
+
readonly devEnvironments?: ProviderDevEnvironments
|
|
570
|
+
/**
|
|
571
|
+
* Whether an environment at this approved access is served the ops channel through which Control reaches its object
|
|
572
|
+
* (ADR-0069), so its deploy gets `FABRIKA_OPS_KEY_DIGESTS`. The release gate is the provider's, so core keeps no
|
|
573
|
+
* release logic. Absent means no environment of this provider is.
|
|
574
|
+
*/
|
|
575
|
+
opsChannel?(access: ProviderApprovedAccess): boolean
|
|
507
576
|
/** Absent means the provider deploys public sources only. */
|
|
508
577
|
readonly source?: ProviderSourceCapabilities
|
|
509
578
|
/**
|
package/src/steps.ts
CHANGED
|
@@ -59,7 +59,9 @@ export const runSteps = async <TStep extends { readonly id: string; readonly des
|
|
|
59
59
|
log(`→ ${step.id}`)
|
|
60
60
|
try {
|
|
61
61
|
await untilCancelled(runOne(step), signal)
|
|
62
|
-
|
|
62
|
+
const finishedAt = Date.now()
|
|
63
|
+
log(`✓ ${step.id} (${((finishedAt - startedAt) / 1000).toFixed(1)}s)`)
|
|
64
|
+
results.push({ id: step.id, status: 'succeeded', startedAt, finishedAt })
|
|
63
65
|
} catch (error) {
|
|
64
66
|
const message = error instanceof Error ? error.message : String(error)
|
|
65
67
|
stopped = true
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ProviderDeploymentNamespace, ProviderNamespacePlan } from './control';
|
|
2
|
+
/** The runtime a contract application's generated entry is written for (`@fabrika/app/build`'s `EntryRuntime`). */
|
|
3
|
+
export type AppEntryRuntime = 'bun' | 'cloudflare';
|
|
4
|
+
/** What a namespace command asks of Control. The `fabrika` command holds the Control origin, the credential and the transport. */
|
|
5
|
+
export interface NamespaceControl {
|
|
6
|
+
create(namespace: ProviderDeploymentNamespace): Promise<unknown>;
|
|
7
|
+
adopt(namespace: ProviderDeploymentNamespace): Promise<unknown>;
|
|
8
|
+
reconcile(namespaceId: string): Promise<unknown>;
|
|
9
|
+
}
|
|
10
|
+
/** A plan, or the namespace Control answered with; the `fabrika` command validates and prints either. */
|
|
11
|
+
export type NamespaceCommandResult = {
|
|
12
|
+
readonly plan: ProviderNamespacePlan;
|
|
13
|
+
} | {
|
|
14
|
+
readonly namespace: unknown;
|
|
15
|
+
};
|
|
16
|
+
export interface NamespaceCommandContext {
|
|
17
|
+
readonly source: Readonly<Record<string, string | undefined>>;
|
|
18
|
+
/** Reads the Control origin and credential, and throws when either is missing: call it after every other input is checked. */
|
|
19
|
+
control(): NamespaceControl;
|
|
20
|
+
}
|
|
21
|
+
export interface NamespaceCli {
|
|
22
|
+
readonly usage: string;
|
|
23
|
+
/** `argv` is the command's own arguments, without the router's `--provider` and `--json`. */
|
|
24
|
+
run(subcommand: string, argv: readonly string[], context: NamespaceCommandContext): Promise<NamespaceCommandResult>;
|
|
25
|
+
}
|
|
26
|
+
export interface AppCli {
|
|
27
|
+
readonly provider: string;
|
|
28
|
+
readonly entryRuntime: AppEntryRuntime;
|
|
29
|
+
/** `fabrika app build`, after the command wrote a contract application's entry; `contract` says whether it did. */
|
|
30
|
+
build?(argv: readonly string[], context: {
|
|
31
|
+
readonly contract: boolean;
|
|
32
|
+
}): Promise<void>;
|
|
33
|
+
deploy?(argv: readonly string[]): Promise<void>;
|
|
34
|
+
/** The text `fabrika app register` hands Control: the application's manifest for one environment. */
|
|
35
|
+
registrationManifest?(options: {
|
|
36
|
+
readonly config: string;
|
|
37
|
+
readonly env: string;
|
|
38
|
+
}): Promise<string>;
|
|
39
|
+
readonly namespaces?: NamespaceCli;
|
|
40
|
+
}
|
|
41
|
+
export interface AppCliModule {
|
|
42
|
+
readonly appCli: AppCli;
|
|
43
|
+
}
|
|
44
|
+
export declare const isAppCli: (value: unknown) => value is AppCli;
|
|
45
|
+
/** `specifier` names the module in the error, so an operator knows which package to upgrade. */
|
|
46
|
+
export declare const appCliFromModule: (value: unknown, expectedProvider: string, specifier: string) => AppCli;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { AppSchema } from '@fabrika/auth-core';
|
|
2
|
+
export declare const APP_PROVIDER: unique symbol;
|
|
3
|
+
export interface ProviderAuthoredApp<Provider extends string = string> {
|
|
4
|
+
readonly [APP_PROVIDER]: Provider;
|
|
5
|
+
}
|
|
6
|
+
export declare const authoredAppProvider: (value: unknown) => string | undefined;
|
|
7
|
+
export declare const isProviderAuthoredApp: <Provider extends string>(value: unknown, provider: Provider) => value is ProviderAuthoredApp<Provider>;
|
|
8
|
+
/** Provider-neutral build inputs and environment requirements declared by an app. */
|
|
9
|
+
export interface AppPipeline {
|
|
10
|
+
/** Source directory relative to the app config file. Defaults to `.`. */
|
|
11
|
+
workerDir?: string;
|
|
12
|
+
/** Shell command that builds the deployable artifact. */
|
|
13
|
+
build?: string;
|
|
14
|
+
/** Secret names that must resolve before deployment. */
|
|
15
|
+
secrets?: string[];
|
|
16
|
+
/** Non-secret variable names that must resolve before deployment. */
|
|
17
|
+
vars?: string[];
|
|
18
|
+
}
|
|
19
|
+
/** App metadata shared by every provider-specific authoring config. */
|
|
20
|
+
export interface AppConfigBase {
|
|
21
|
+
/** Stable app id, unique within one control plane. */
|
|
22
|
+
id: string;
|
|
23
|
+
/** The app-owned authorization vocabulary reconciled into IAM. */
|
|
24
|
+
schema?: AppSchema;
|
|
25
|
+
/** Provider-neutral build and environment requirements. */
|
|
26
|
+
pipeline?: AppPipeline;
|
|
27
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type ControlProvider, type ProviderDeployInput } from './control';
|
|
2
|
+
/** What the suite hands a fake cloud: `accepted` is called once the cloud has taken the deploy's operation. */
|
|
3
|
+
export interface ConformanceCloudHooks {
|
|
4
|
+
accepted(): void;
|
|
5
|
+
}
|
|
6
|
+
/** One provider over one fresh fake cloud. */
|
|
7
|
+
export interface ConformanceCloud {
|
|
8
|
+
readonly provider: ControlProvider;
|
|
9
|
+
/** A deploy the fake cloud accepts and lets succeed. Each case sets `dryRun`, `signal` and `events` itself. */
|
|
10
|
+
readonly input: Omit<ProviderDeployInput, 'dryRun' | 'signal' | 'events'>;
|
|
11
|
+
/** The credentials the provider holds itself (a cloud token, a signed URL); no event may carry one. */
|
|
12
|
+
readonly credentials: readonly string[];
|
|
13
|
+
/** Every request that changed the cloud: a write, a created resource, started work. A preview the cloud runs is none. */
|
|
14
|
+
mutations(): readonly string[];
|
|
15
|
+
/** Every request to stop or remove the deploy's work: a cancelled build or job, a deleted version. */
|
|
16
|
+
stops(): readonly string[];
|
|
17
|
+
}
|
|
18
|
+
export interface ControlProviderConformanceHarness {
|
|
19
|
+
/** Called once per case, and once per event in the lease-loss sweep. */
|
|
20
|
+
create(hooks: ConformanceCloudHooks): ConformanceCloud;
|
|
21
|
+
}
|
|
22
|
+
export declare function describeControlProviderConformance(name: string, harness: ControlProviderConformanceHarness): void;
|
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
import type { AppSchema, ContractArtifact, ManagedEnvironment } from '@fabrika/auth-core';
|
|
2
|
+
import type { InstallationBrand } from '@fabrika/proxy-core/brand';
|
|
3
|
+
import type { JsonValue, ProviderEnvelope } from './json';
|
|
4
|
+
/** Events whose persistence and presentation belong to the control plane. */
|
|
5
|
+
export interface ProviderRunEvents {
|
|
6
|
+
log(line: string): void;
|
|
7
|
+
/** Atomically persist the external operation id and, when supplied, its credential-free checkpoint. */
|
|
8
|
+
externalId(id: string, state?: JsonValue): Promise<void>;
|
|
9
|
+
/** Replace the credential-free checkpoint while this run remains active. */
|
|
10
|
+
checkpoint(state: JsonValue): Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* A step boundary: Control checks its lease and reads the run's cancellation request. Rejects with a
|
|
13
|
+
* `ProviderInvocationAbort` after aborting the run's signal. A long step calls it at its own boundaries.
|
|
14
|
+
*/
|
|
15
|
+
boundary(): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Complete platform-owned runtime state, parsed by Control (`parseManagedEnvironment`). A provider writes each entry as
|
|
19
|
+
* a secret when it is a `credential`, removes one whose value is null, and removes the stale ones `staleManagedVariables` names.
|
|
20
|
+
*/
|
|
21
|
+
export type ProviderManagedEnvironment = ManagedEnvironment;
|
|
22
|
+
/** Provider-neutral coordinates for obtaining one app revision. */
|
|
23
|
+
export interface ProviderSource {
|
|
24
|
+
readonly repoUrl: string;
|
|
25
|
+
readonly ref: string;
|
|
26
|
+
readonly workerDir?: string;
|
|
27
|
+
readonly buildCommand?: string;
|
|
28
|
+
readonly configPath?: string;
|
|
29
|
+
}
|
|
30
|
+
/** App data needed by a control provider. */
|
|
31
|
+
export interface ProviderApp {
|
|
32
|
+
readonly id: string;
|
|
33
|
+
readonly source: ProviderSource;
|
|
34
|
+
}
|
|
35
|
+
/** One provider-owned placement boundary shared by zero or more app environments. */
|
|
36
|
+
export interface ProviderDeploymentNamespace {
|
|
37
|
+
readonly id: string;
|
|
38
|
+
readonly env: string;
|
|
39
|
+
readonly exclusiveAppId?: string;
|
|
40
|
+
readonly target: ProviderEnvelope;
|
|
41
|
+
}
|
|
42
|
+
export { FRAMEWORK_RELEASE_PATTERN } from '@fabrika/auth-core';
|
|
43
|
+
/**
|
|
44
|
+
* The access Control approved for one application environment (ADR-0050): the contract the proxy enforces and the
|
|
45
|
+
* IAM vocabulary compiled from it. A provider publishes a contract application's endpoint table and reconciles its
|
|
46
|
+
* IAM schema from this, never from the artifact or a runner's report.
|
|
47
|
+
*/
|
|
48
|
+
export interface ProviderApprovedAccess {
|
|
49
|
+
/** The contract in this platform release's model, upcast from the application's own release's artifact (ADR-0062). */
|
|
50
|
+
readonly contract: ContractArtifact;
|
|
51
|
+
/**
|
|
52
|
+
* The digest the application's own framework release computes for its contract, which it compares with
|
|
53
|
+
* `FABRIKA_CONTRACT_DIGEST` at startup. It is `contractDigest(contract)` only when the application is on this
|
|
54
|
+
* release's artifact format, so a provider never derives it from `contract`.
|
|
55
|
+
*/
|
|
56
|
+
readonly contractDigest: string;
|
|
57
|
+
readonly schema: AppSchema;
|
|
58
|
+
/**
|
|
59
|
+
* The `@fabrika/check` release whose reader read the application (`FRAMEWORK_RELEASE_PATTERN`): the newest patch of
|
|
60
|
+
* the framework minor it pins (ADR-0062). Its build checks with that release.
|
|
61
|
+
*/
|
|
62
|
+
readonly checker: string;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* `dev`: the environment follows a workspace through a platform-owned launcher, which its provider starts instead of the
|
|
66
|
+
* serving entry (ADR-0043). The kind is Control's registry, never the application's.
|
|
67
|
+
*/
|
|
68
|
+
export type ProviderEnvironmentKind = 'standard' | 'dev';
|
|
69
|
+
/** Persisted deploy data for one app environment. */
|
|
70
|
+
export interface ProviderEnvironment {
|
|
71
|
+
readonly appId: string;
|
|
72
|
+
readonly env: string;
|
|
73
|
+
/** Absent means `standard`. */
|
|
74
|
+
readonly kind?: ProviderEnvironmentKind;
|
|
75
|
+
readonly domain?: string;
|
|
76
|
+
/** Canonical externally reachable HTTP(S) origin; independent of provider domain routing. */
|
|
77
|
+
readonly publicOrigin?: string;
|
|
78
|
+
/**
|
|
79
|
+
* The exact origins whose pages may call the environment's cross-origin endpoints, as the operator listed them in
|
|
80
|
+
* Control's registry (ADR-0022 "Cross-origin endpoints"). Absent when it lists none. A provider writes them into the
|
|
81
|
+
* application's proxy manifest entry as `crossOrigins`.
|
|
82
|
+
*/
|
|
83
|
+
readonly crossOrigins?: readonly string[];
|
|
84
|
+
/**
|
|
85
|
+
* The origin of the environment's active Operations ingest DSN, never the DSN: a contract application's proxy admits
|
|
86
|
+
* it in `connect-src`. Control sets it on a deploy; absent while the environment has no active ingest.
|
|
87
|
+
*/
|
|
88
|
+
readonly operationsOrigin?: string;
|
|
89
|
+
readonly namespace?: ProviderDeploymentNamespace;
|
|
90
|
+
readonly target: ProviderEnvelope;
|
|
91
|
+
readonly artifact: ProviderEnvelope;
|
|
92
|
+
/**
|
|
93
|
+
* The environment's approved contract, from Control's `approvedSnapshot`. Absent when nothing is approved: then a
|
|
94
|
+
* provider publishes no endpoint table and reconciles no IAM schema, whatever the artifact claims (ADR-0050).
|
|
95
|
+
* Control refuses a non-dry deploy without it.
|
|
96
|
+
*/
|
|
97
|
+
readonly access?: ProviderApprovedAccess;
|
|
98
|
+
}
|
|
99
|
+
/** Raw registration data passed to provider-owned validation and normalization. */
|
|
100
|
+
export interface ProviderRegistrationInput {
|
|
101
|
+
readonly app: ProviderApp;
|
|
102
|
+
readonly environment: ProviderEnvironment;
|
|
103
|
+
}
|
|
104
|
+
/** Canonical registration returned after provider-owned validation and normalization. */
|
|
105
|
+
export type ProviderRegistration = ProviderRegistrationInput;
|
|
106
|
+
/** Provider-neutral input for resolving one requested source ref to an immutable Git object id. */
|
|
107
|
+
export interface ProviderSourceResolutionInput {
|
|
108
|
+
readonly runId: string;
|
|
109
|
+
readonly app: ProviderApp;
|
|
110
|
+
readonly environment: ProviderEnvironment;
|
|
111
|
+
/** An immutable commit already recorded by the trigger, which resolution must verify exactly. */
|
|
112
|
+
readonly expectedCommitSha?: string;
|
|
113
|
+
readonly sourceBinding?: ProviderSourceBinding;
|
|
114
|
+
readonly signal: AbortSignal;
|
|
115
|
+
}
|
|
116
|
+
/** The exact source revision a provider resolved before starting deploy work. */
|
|
117
|
+
export interface ProviderSourceResolution {
|
|
118
|
+
readonly commitSha: string;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Credential-free selector of the GitHub connection and installation that read a private source
|
|
122
|
+
* (ADR-0029). Absent means a public source; an installation id alone never selects a credential.
|
|
123
|
+
*/
|
|
124
|
+
export interface ProviderSourceBinding {
|
|
125
|
+
readonly connectionId: string;
|
|
126
|
+
readonly installationId: number;
|
|
127
|
+
}
|
|
128
|
+
/** How private GitHub sources reach a provider (ADR-0029): one keyed connection's App and installation. */
|
|
129
|
+
export interface ProviderSourceCapabilities {
|
|
130
|
+
readonly binding: 'connection';
|
|
131
|
+
}
|
|
132
|
+
/** One control-plane deploy invocation. */
|
|
133
|
+
export interface ProviderDeployInput {
|
|
134
|
+
readonly runId: string;
|
|
135
|
+
readonly app: ProviderApp;
|
|
136
|
+
readonly environment: ProviderEnvironment;
|
|
137
|
+
readonly secrets: Readonly<Record<string, string>>;
|
|
138
|
+
readonly vars: Readonly<Record<string, string>>;
|
|
139
|
+
/** Platform-owned runtime configuration, separate from operator-managed application vars. */
|
|
140
|
+
readonly managedEnvironment: ProviderManagedEnvironment;
|
|
141
|
+
readonly dryRun: boolean;
|
|
142
|
+
readonly sourceBinding?: ProviderSourceBinding;
|
|
143
|
+
/** Optional short-lived, run-scoped Operations artifact destination. */
|
|
144
|
+
readonly artifactUpload?: {
|
|
145
|
+
readonly url: string;
|
|
146
|
+
readonly bearer: string;
|
|
147
|
+
readonly appId: string;
|
|
148
|
+
readonly environment: string;
|
|
149
|
+
readonly serviceKey: string;
|
|
150
|
+
readonly release: string;
|
|
151
|
+
readonly runId: string;
|
|
152
|
+
};
|
|
153
|
+
readonly signal: AbortSignal;
|
|
154
|
+
readonly events: ProviderRunEvents;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* A terminal provider operation result. `succeeded` means the version is live: Control then writes the environment's
|
|
158
|
+
* approved IAM schema itself (ADR-0011), so a provider never calls IAM for an application.
|
|
159
|
+
*/
|
|
160
|
+
export interface ProviderTerminalOutcome {
|
|
161
|
+
readonly state: 'succeeded' | 'failed';
|
|
162
|
+
readonly exitCode?: number;
|
|
163
|
+
/** Whether provider-local release artifacts reached Operations. */
|
|
164
|
+
readonly artifactState?: 'complete' | 'incomplete' | 'not_applicable';
|
|
165
|
+
}
|
|
166
|
+
/** Running means external settlement is unresolved, including an ambiguous submission response. */
|
|
167
|
+
export type ProviderDeployOutcome = ProviderTerminalOutcome | {
|
|
168
|
+
readonly state: 'running';
|
|
169
|
+
};
|
|
170
|
+
/** Settled does not imply rollback: when activation won before cancellation, `outcome` is `succeeded`. */
|
|
171
|
+
export type ProviderCancelOutcome = {
|
|
172
|
+
readonly state: 'pending';
|
|
173
|
+
} | {
|
|
174
|
+
readonly state: 'settled';
|
|
175
|
+
readonly outcome?: 'succeeded' | 'failed';
|
|
176
|
+
};
|
|
177
|
+
/** Losing invocation ownership must never be interpreted as permission to cancel external work. */
|
|
178
|
+
export declare class ProviderInvocationAbort extends Error {
|
|
179
|
+
readonly kind: 'lease_lost' | 'cancel_requested';
|
|
180
|
+
constructor(kind: 'lease_lost' | 'cancel_requested');
|
|
181
|
+
}
|
|
182
|
+
export declare const isLeaseLoss: (reason: unknown) => boolean;
|
|
183
|
+
/** A signal that aborts with `signal`'s reason only when the lease is lost, never on a cancel. */
|
|
184
|
+
export declare const followLeaseLoss: (signal: AbortSignal) => AbortSignal;
|
|
185
|
+
/** Resolve after `ms`, or reject with the signal's reason once it aborts. */
|
|
186
|
+
export declare const abortableWait: (ms: number, signal: AbortSignal) => Promise<void>;
|
|
187
|
+
/** A platform-owned run that can be cancelled or reconciled. */
|
|
188
|
+
export interface ProviderRunReference {
|
|
189
|
+
readonly runId: string;
|
|
190
|
+
readonly externalId: string;
|
|
191
|
+
readonly environment: ProviderEnvironment;
|
|
192
|
+
/** Credential-free provider-owned checkpoint recorded by the run's last owner. */
|
|
193
|
+
readonly providerState?: JsonValue;
|
|
194
|
+
/** When the run started (`started_at`, else `created_at`), in epoch milliseconds; bounds how long a provider polls. */
|
|
195
|
+
readonly startedAtMs?: number;
|
|
196
|
+
}
|
|
197
|
+
export interface ProviderCancelInput extends ProviderRunReference {
|
|
198
|
+
readonly signal: AbortSignal;
|
|
199
|
+
}
|
|
200
|
+
export interface ProviderReconcileInput extends ProviderRunReference {
|
|
201
|
+
readonly signal: AbortSignal;
|
|
202
|
+
/** Append a line to the run's log. */
|
|
203
|
+
log?(line: string): Promise<void>;
|
|
204
|
+
}
|
|
205
|
+
/** The latest state reported while reconciling a platform-owned run. */
|
|
206
|
+
export interface ProviderReconcileOutcome {
|
|
207
|
+
readonly state: 'running' | 'succeeded' | 'failed';
|
|
208
|
+
readonly exitCode?: number;
|
|
209
|
+
/** See `ProviderTerminalOutcome.artifactState`. */
|
|
210
|
+
readonly artifactState?: 'complete' | 'incomplete' | 'not_applicable';
|
|
211
|
+
/** The settled run was a preview: core projects it as a dry run. */
|
|
212
|
+
readonly dryRun?: boolean;
|
|
213
|
+
}
|
|
214
|
+
/** Write one secret value to provider-managed storage. */
|
|
215
|
+
export interface ProviderSecretPutInput {
|
|
216
|
+
readonly environment: ProviderEnvironment;
|
|
217
|
+
readonly name: string;
|
|
218
|
+
readonly value: string;
|
|
219
|
+
}
|
|
220
|
+
/** Remove one secret value from provider-managed storage. */
|
|
221
|
+
export interface ProviderSecretDeleteInput {
|
|
222
|
+
readonly environment: ProviderEnvironment;
|
|
223
|
+
readonly name: string;
|
|
224
|
+
}
|
|
225
|
+
/** Opaque reference persisted by core instead of the provider-managed secret value. */
|
|
226
|
+
export interface ProviderSecretPutResult {
|
|
227
|
+
readonly valueRef: string;
|
|
228
|
+
}
|
|
229
|
+
/** Optional edit-time secret storage owned by a provider. */
|
|
230
|
+
export interface ProviderManagedSecrets {
|
|
231
|
+
put(input: ProviderSecretPutInput): Promise<ProviderSecretPutResult>;
|
|
232
|
+
delete(input: ProviderSecretDeleteInput): Promise<void>;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* A namespace lifecycle failure an operator can act on.
|
|
236
|
+
*
|
|
237
|
+
* `code` is a STABLE identifier — a platform error code, or one this provider defines for a lifecycle
|
|
238
|
+
* invariant — so core can record and a console can render the CLASS of a failure instead of matching
|
|
239
|
+
* prose. `message` is the provider's own summary; `detail` carries the upstream's words VERBATIM.
|
|
240
|
+
* Neither is safe to persist as-is: core redacts both before they reach a row or a log, because only
|
|
241
|
+
* core knows what a credential looks like in this installation.
|
|
242
|
+
*/
|
|
243
|
+
export declare class ProviderNamespaceError extends Error {
|
|
244
|
+
readonly code: string;
|
|
245
|
+
readonly retryable: boolean;
|
|
246
|
+
readonly detail?: string | undefined;
|
|
247
|
+
constructor(message: string, code: string, retryable: boolean, detail?: string | undefined);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* A provider-managed secret write the provider refuses on the app's own terms — a value naming a
|
|
251
|
+
* service the app does not own, say. `message` carries the secret's name and the offending host,
|
|
252
|
+
* never the value, so core may hand it to the caller as a 4xx.
|
|
253
|
+
*/
|
|
254
|
+
export declare class ProviderSecretError extends Error {
|
|
255
|
+
readonly code: string;
|
|
256
|
+
constructor(message: string, code: string);
|
|
257
|
+
}
|
|
258
|
+
/** Persist a canonical namespace target immediately after one external mutation succeeds. */
|
|
259
|
+
export interface ProviderNamespaceEvents {
|
|
260
|
+
checkpoint(namespace: ProviderDeploymentNamespace): Promise<void>;
|
|
261
|
+
/** A step boundary: Control checks the namespace lease and, for a deploy, the run's cancellation request. */
|
|
262
|
+
boundary(): Promise<void>;
|
|
263
|
+
}
|
|
264
|
+
/** One app environment assigned to a namespace, as its proxy routes it. */
|
|
265
|
+
export interface ProviderNamespaceMember {
|
|
266
|
+
readonly appId: string;
|
|
267
|
+
readonly env: string;
|
|
268
|
+
readonly domain?: string;
|
|
269
|
+
/** See `ProviderEnvironment.crossOrigins`: the namespace proxy's entry for this member lists them. */
|
|
270
|
+
readonly crossOrigins?: readonly string[];
|
|
271
|
+
readonly artifact: ProviderEnvelope;
|
|
272
|
+
/**
|
|
273
|
+
* The secret the namespace proxy authenticates itself to this application environment with (ADR-0022 rule 5).
|
|
274
|
+
* Control generates it once and keeps it in its vault; absent when Control has no vault.
|
|
275
|
+
*/
|
|
276
|
+
readonly proxySecret?: string;
|
|
277
|
+
/**
|
|
278
|
+
* The origin of this environment's active Operations ingest DSN, never the DSN itself: a contract application's
|
|
279
|
+
* proxy admits it in `connect-src`, as the framework does. Absent while the environment has no active ingest.
|
|
280
|
+
*/
|
|
281
|
+
readonly operationsOrigin?: string;
|
|
282
|
+
/** See `ProviderEnvironment.access`: the proxy table of a contract application comes only from here. */
|
|
283
|
+
readonly access?: ProviderApprovedAccess;
|
|
284
|
+
}
|
|
285
|
+
/** One idempotent namespace lifecycle invocation. */
|
|
286
|
+
export interface ProviderNamespaceMutationInput {
|
|
287
|
+
readonly namespace: ProviderDeploymentNamespace;
|
|
288
|
+
/** Every app environment assigned to the namespace, read under its lease. */
|
|
289
|
+
readonly members: readonly ProviderNamespaceMember[];
|
|
290
|
+
/**
|
|
291
|
+
* The installation's client logo for the pages the namespace's proxy answers with (ADR-0066), read from IAM for this
|
|
292
|
+
* mutation; absent or null shows the fabrika mark. A provider passes it only to a proxy that can parse it.
|
|
293
|
+
*/
|
|
294
|
+
readonly brand?: InstallationBrand | null;
|
|
295
|
+
readonly signal: AbortSignal;
|
|
296
|
+
readonly events: ProviderNamespaceEvents;
|
|
297
|
+
}
|
|
298
|
+
/** `settled` is false while the rebuild is still in flight; core then hands it to a `reconcile` job. */
|
|
299
|
+
export interface ProviderNamespaceRepublishResult {
|
|
300
|
+
readonly namespace: ProviderDeploymentNamespace;
|
|
301
|
+
readonly settled: boolean;
|
|
302
|
+
}
|
|
303
|
+
/** Rebuild a ready namespace's entry point from its current members. */
|
|
304
|
+
export interface ProviderNamespaceRepublishInput extends ProviderNamespaceMutationInput {
|
|
305
|
+
/** `started`: return once the rebuild is submitted. `active`: return once it is live. */
|
|
306
|
+
readonly wait: 'started' | 'active';
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* The `code` of a `ProviderNamespaceError` an update plan throws when the requested target changes
|
|
310
|
+
* something the provider cannot move in place. Core answers it as a conflict that names the supported path.
|
|
311
|
+
*/
|
|
312
|
+
export declare const PROVIDER_NAMESPACE_IMMUTABLE = "namespaceImmutable";
|
|
313
|
+
/**
|
|
314
|
+
* The `code` of a `ProviderNamespaceError` an update apply throws when the live change MAY have been made
|
|
315
|
+
* and is not confirmed. Any other failure means nothing changed. Applying the same plan again must converge.
|
|
316
|
+
*/
|
|
317
|
+
export declare const PROVIDER_NAMESPACE_UPDATE_UNCONFIRMED = "namespaceUpdateUnconfirmed";
|
|
318
|
+
/** One provider-described change, safe to audit: names and values only, never a credential. */
|
|
319
|
+
export interface ProviderNamespaceChange {
|
|
320
|
+
readonly field: string;
|
|
321
|
+
readonly from: string | null;
|
|
322
|
+
readonly to: string;
|
|
323
|
+
}
|
|
324
|
+
/** The validated result of merging a requested target over the stored one. `change` is null for a no-op. */
|
|
325
|
+
export interface ProviderNamespaceUpdatePlan {
|
|
326
|
+
readonly namespace: ProviderDeploymentNamespace;
|
|
327
|
+
readonly change: ProviderNamespaceChange | null;
|
|
328
|
+
}
|
|
329
|
+
/** Apply one plan. `next` is the planned namespace, with the same coordinates as `namespace`. */
|
|
330
|
+
export interface ProviderNamespaceUpdateInput {
|
|
331
|
+
readonly namespace: ProviderDeploymentNamespace;
|
|
332
|
+
readonly next: ProviderDeploymentNamespace;
|
|
333
|
+
/** The namespace an earlier, unconfirmed update planned; the live resources may already match it. */
|
|
334
|
+
readonly unconfirmed?: ProviderDeploymentNamespace;
|
|
335
|
+
readonly signal: AbortSignal;
|
|
336
|
+
}
|
|
337
|
+
/** In-place change of a settled namespace. */
|
|
338
|
+
export interface ProviderNamespaceUpdates {
|
|
339
|
+
/**
|
|
340
|
+
* Merge `requested` — whose payload may name only what changes — over the stored `namespace` and
|
|
341
|
+
* validate the result. Pure. A change that cannot be made in place throws `PROVIDER_NAMESPACE_IMMUTABLE`.
|
|
342
|
+
*/
|
|
343
|
+
plan(namespace: ProviderDeploymentNamespace, requested: ProviderEnvelope): ProviderNamespaceUpdatePlan;
|
|
344
|
+
/** Move the live resources to `next` and return the target core stores. */
|
|
345
|
+
apply(input: ProviderNamespaceUpdateInput): Promise<ProviderDeploymentNamespace>;
|
|
346
|
+
}
|
|
347
|
+
/** One registration whose provider coordinates do not exist until namespace services are prepared. */
|
|
348
|
+
export interface ProviderRegistrationPreparationInput {
|
|
349
|
+
readonly registration: ProviderRegistration;
|
|
350
|
+
readonly signal: AbortSignal;
|
|
351
|
+
}
|
|
352
|
+
/** One provider-defined operator choice. Core does not interpret its id or description. */
|
|
353
|
+
export interface ProviderNamespacePreset {
|
|
354
|
+
readonly id: string;
|
|
355
|
+
readonly label: string;
|
|
356
|
+
readonly description: string;
|
|
357
|
+
readonly requiresExclusiveApp: boolean;
|
|
358
|
+
}
|
|
359
|
+
/** Provider-neutral coordinates plus opaque provider options for a namespace preview. */
|
|
360
|
+
export interface ProviderNamespacePlanInput {
|
|
361
|
+
readonly id: string;
|
|
362
|
+
readonly env: string;
|
|
363
|
+
readonly preset: string;
|
|
364
|
+
readonly exclusiveAppId?: string;
|
|
365
|
+
readonly options?: JsonValue;
|
|
366
|
+
}
|
|
367
|
+
/** One safe, human-readable provider fact. Values must never contain credentials. */
|
|
368
|
+
export interface ProviderNamespaceFact {
|
|
369
|
+
readonly label: string;
|
|
370
|
+
readonly value: string;
|
|
371
|
+
/**
|
|
372
|
+
* Marks a fact that NAMES A LIVE PROVIDER RESOURCE, not a policy choice. Core deletes no provider
|
|
373
|
+
* resource, so removal has to tell the operator what it stopped tracking — and only the provider
|
|
374
|
+
* knows which of its facts is a real thing with an id. Absent means "not a resource"; a fact is
|
|
375
|
+
* marked only when its value identifies something that outlives the namespace row.
|
|
376
|
+
*/
|
|
377
|
+
readonly resource?: true;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* One public host this namespace's own entry point answers on, with the listener port that publishes it.
|
|
381
|
+
*
|
|
382
|
+
* Only the provider knows how a host comes to exist — Zerops generates one per published HTTP port — and
|
|
383
|
+
* core knows which of them an application environment already claims, so the provider names them and core
|
|
384
|
+
* marks them. A namespace whose domains are bound out of band names none: the provider cannot see them.
|
|
385
|
+
*/
|
|
386
|
+
export interface ProviderNamespaceHost {
|
|
387
|
+
readonly host: string;
|
|
388
|
+
readonly port: number;
|
|
389
|
+
}
|
|
390
|
+
/** Provider-owned operator copy for a planned or persisted namespace. */
|
|
391
|
+
export interface ProviderNamespacePresentation {
|
|
392
|
+
readonly preset: string;
|
|
393
|
+
readonly title: string;
|
|
394
|
+
readonly facts: readonly ProviderNamespaceFact[];
|
|
395
|
+
readonly instructions: readonly string[];
|
|
396
|
+
/** The hosts this namespace can serve, so an operator can name one as an environment's domain. */
|
|
397
|
+
readonly hosts?: readonly ProviderNamespaceHost[];
|
|
398
|
+
}
|
|
399
|
+
/** A mutation-free preview that can be submitted unchanged to namespace creation. */
|
|
400
|
+
export interface ProviderNamespacePlan {
|
|
401
|
+
readonly namespace: ProviderDeploymentNamespace;
|
|
402
|
+
readonly presentation: ProviderNamespacePresentation;
|
|
403
|
+
}
|
|
404
|
+
/** Optional operator surface for providers that offer opinionated namespace presets. */
|
|
405
|
+
export interface ProviderNamespaceOperator {
|
|
406
|
+
readonly presets: readonly ProviderNamespacePreset[];
|
|
407
|
+
plan(input: ProviderNamespacePlanInput): ProviderNamespacePlan;
|
|
408
|
+
present(namespace: ProviderDeploymentNamespace): ProviderNamespacePresentation;
|
|
409
|
+
}
|
|
410
|
+
/** The app-owned claims of one namespace whose resources Control is asked to confirm gone. */
|
|
411
|
+
export interface ProviderClaimedResourcesInput {
|
|
412
|
+
readonly namespace: ProviderDeploymentNamespace;
|
|
413
|
+
readonly resourceKeys: readonly string[];
|
|
414
|
+
readonly signal: AbortSignal;
|
|
415
|
+
}
|
|
416
|
+
/** What the provider observed for one claimed resource. */
|
|
417
|
+
export interface ProviderClaimedResource {
|
|
418
|
+
readonly resourceKey: string;
|
|
419
|
+
readonly exists: boolean;
|
|
420
|
+
}
|
|
421
|
+
/** Optional placement lifecycle owned by providers that use deployment namespaces. */
|
|
422
|
+
export interface ProviderNamespaceCapabilities {
|
|
423
|
+
normalize(namespace: ProviderDeploymentNamespace): ProviderDeploymentNamespace;
|
|
424
|
+
/** Namespace-owned resources that must be reserved before provisioning starts. */
|
|
425
|
+
namespaceResourceClaims(namespace: ProviderDeploymentNamespace): readonly string[];
|
|
426
|
+
/** App-owned resources derived from one canonical normalized registration. */
|
|
427
|
+
registrationResourceClaims(registration: ProviderRegistration): readonly string[];
|
|
428
|
+
/**
|
|
429
|
+
* Materialize app-owned provider resources and return their canonical coordinates.
|
|
430
|
+
* Core acquires every registration resource claim before invoking this mutation.
|
|
431
|
+
*/
|
|
432
|
+
prepareRegistration?(input: ProviderRegistrationPreparationInput): Promise<ProviderRegistration>;
|
|
433
|
+
provision(input: ProviderNamespaceMutationInput): Promise<ProviderDeploymentNamespace>;
|
|
434
|
+
reconcile(input: ProviderNamespaceMutationInput): Promise<ProviderDeploymentNamespace>;
|
|
435
|
+
/** Absent means the namespace has no entry point that follows its members. */
|
|
436
|
+
republish?(input: ProviderNamespaceRepublishInput): Promise<ProviderNamespaceRepublishResult>;
|
|
437
|
+
/**
|
|
438
|
+
* Read-only: whether each claimed resource still exists, one answer per key. Throws when it cannot tell, so Control
|
|
439
|
+
* releases a claim only over a resource the provider confirmed gone. Absent means no claim can be released.
|
|
440
|
+
*/
|
|
441
|
+
claimedResources?(input: ProviderClaimedResourcesInput): Promise<readonly ProviderClaimedResource[]>;
|
|
442
|
+
/** Absent means the provider changes no namespace in place. */
|
|
443
|
+
readonly updates?: ProviderNamespaceUpdates;
|
|
444
|
+
readonly operator?: ProviderNamespaceOperator;
|
|
445
|
+
}
|
|
446
|
+
export interface ProviderAuthoring {
|
|
447
|
+
/** The file NAMES (no `/`) that configure the provider's deploy; a workspace changes none of them, at any depth. */
|
|
448
|
+
readonly protectedFiles: readonly string[];
|
|
449
|
+
}
|
|
450
|
+
/** One call on the service of a `dev` environment, between its bootstrap runs. */
|
|
451
|
+
export interface ProviderDevEnvironmentInput {
|
|
452
|
+
readonly environment: ProviderEnvironment;
|
|
453
|
+
readonly signal: AbortSignal;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* The service of a `dev` environment outside a run (ADR-0049): Control stops it after an idle period and starts it on
|
|
457
|
+
* the next commit. Files the launcher wrote survive both.
|
|
458
|
+
*/
|
|
459
|
+
export interface ProviderDevEnvironments {
|
|
460
|
+
stop(input: ProviderDevEnvironmentInput): Promise<void>;
|
|
461
|
+
start(input: ProviderDevEnvironmentInput): Promise<void>;
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* The control-plane capability bundle selected statically by one composition root.
|
|
465
|
+
*
|
|
466
|
+
* Core owns no provider registry. The selected provider validates registration envelopes and owns
|
|
467
|
+
* every provider-specific deploy lifecycle capability.
|
|
468
|
+
*/
|
|
469
|
+
export interface ControlProvider {
|
|
470
|
+
readonly id: string;
|
|
471
|
+
/** Absent means core stores the registration as submitted. */
|
|
472
|
+
normalizeRegistration?(input: ProviderRegistrationInput): ProviderRegistration;
|
|
473
|
+
resolveSource?(input: ProviderSourceResolutionInput): Promise<ProviderSourceResolution>;
|
|
474
|
+
/**
|
|
475
|
+
* Mutation-free check of a candidate's compatibility with its placement. Control calls it before publishing the
|
|
476
|
+
* snapshot, under the namespace lease, on every publication attempt. The candidate is not yet approved access.
|
|
477
|
+
*/
|
|
478
|
+
validateAccess?(input: {
|
|
479
|
+
readonly environment: ProviderEnvironment;
|
|
480
|
+
readonly access: ProviderApprovedAccess;
|
|
481
|
+
}): void;
|
|
482
|
+
deploy(input: ProviderDeployInput): Promise<ProviderDeployOutcome>;
|
|
483
|
+
cancel?(input: ProviderCancelInput): Promise<ProviderCancelOutcome>;
|
|
484
|
+
reconcile?(input: ProviderReconcileInput): Promise<ProviderReconcileOutcome>;
|
|
485
|
+
readonly secrets?: ProviderManagedSecrets;
|
|
486
|
+
readonly namespaces?: ProviderNamespaceCapabilities;
|
|
487
|
+
/** MCP authoring (ADR-0049): an environment of this provider can be an authoring target. Absent means none can. */
|
|
488
|
+
readonly authoring?: ProviderAuthoring;
|
|
489
|
+
/**
|
|
490
|
+
* Every application environment gets `FABRIKA_PROXY_SECRET`, with or without a namespace proxy: the provider's own
|
|
491
|
+
* hop to the application carries it (Cloudflare's Worker to its Durable Object, ADR-0065). Absent means only a
|
|
492
|
+
* namespace proxy's environments get one.
|
|
493
|
+
*/
|
|
494
|
+
readonly applicationHopSecret?: true;
|
|
495
|
+
/** Absent means the provider runs no `dev` environment, and Control refuses one. */
|
|
496
|
+
readonly devEnvironments?: ProviderDevEnvironments;
|
|
497
|
+
/**
|
|
498
|
+
* Whether an environment at this approved access is served the ops channel through which Control reaches its object
|
|
499
|
+
* (ADR-0069), so its deploy gets `FABRIKA_OPS_KEY_DIGESTS`. The release gate is the provider's, so core keeps no
|
|
500
|
+
* release logic. Absent means no environment of this provider is.
|
|
501
|
+
*/
|
|
502
|
+
opsChannel?(access: ProviderApprovedAccess): boolean;
|
|
503
|
+
/** Absent means the provider deploys public sources only. */
|
|
504
|
+
readonly source?: ProviderSourceCapabilities;
|
|
505
|
+
/**
|
|
506
|
+
* The variable names this artifact declares, so core can refuse one it would silently ignore
|
|
507
|
+
* ([ADR-0035](../../../docs/decisions/0035-the-platform-owns-the-application-iam-issuer.md)).
|
|
508
|
+
*
|
|
509
|
+
* OPTIONAL, and `undefined` is the honest answer rather than an empty set: a provider whose artifact
|
|
510
|
+
* names a config path in the repository cannot know what that config declares, and guessing would
|
|
511
|
+
* refuse a variable that works. A provider that CAN answer — one whose artifact is the compiled
|
|
512
|
+
* manifest — constrains what an operator may set.
|
|
513
|
+
*/
|
|
514
|
+
declaredVariables?(input: {
|
|
515
|
+
readonly artifact: ProviderEnvelope;
|
|
516
|
+
}): readonly string[] | undefined;
|
|
517
|
+
/**
|
|
518
|
+
* The repository-relative directory this provider executes the application from, normalized as its execution
|
|
519
|
+
* normalizes it (`''` for the repository root), or null for a root execution refuses. Control reads the run's
|
|
520
|
+
* access snapshot there (ADR-0050). Absent means the repository root.
|
|
521
|
+
*/
|
|
522
|
+
applicationRoot?(source: ProviderSource): string | null;
|
|
523
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The platform commands a provider MAY offer. Every one of them is optional — a provider declares the
|
|
3
|
+
* subset it implements and the CLI prints its usage for anything else.
|
|
4
|
+
*
|
|
5
|
+
* `install` is the newest and the narrowest in support: it BRINGS UP an installation that does not
|
|
6
|
+
* exist yet, generating and placing every credential it needs. Only Zerops declares it, because only
|
|
7
|
+
* there is a from-scratch bring-up a sequence of API calls; on Cloudflare the same ground is covered by
|
|
8
|
+
* `init` plus the workflow it scaffolds.
|
|
9
|
+
*
|
|
10
|
+
* `admin` admits the FIRST HUMAN to an installation that already runs. It is a verb of its own rather
|
|
11
|
+
* than a step of `install`, because it is the one command that is useful long after the bring-up: an
|
|
12
|
+
* installation whose only administrator left needs it, and a bring-up that got as far as the console
|
|
13
|
+
* does not need to be repeated to use it.
|
|
14
|
+
*
|
|
15
|
+
* `upgrade` rolls a RUNNING installation onto a newer published fabrika release. It is separate from
|
|
16
|
+
* `deploy` because the two run in different places: `deploy` runs inside the operator's own pipeline
|
|
17
|
+
* and brings an installation to the checkout it was handed, while `upgrade` runs on the operator's
|
|
18
|
+
* machine and moves the pin that pipeline reads. A provider whose operator edits no pin does not
|
|
19
|
+
* declare it.
|
|
20
|
+
*
|
|
21
|
+
* `guide` prints the operator's procedure for the whole bring-up, with the traps the per-command usage
|
|
22
|
+
* cannot show. It ships with the provider package, so an agent reads the guide of the release it runs.
|
|
23
|
+
*/
|
|
24
|
+
export type InstallationCommand = 'init' | 'install' | 'plan' | 'deploy' | 'admin' | 'upgrade' | 'guide';
|
|
25
|
+
export declare const isInstallationCommand: (value: unknown) => value is InstallationCommand;
|
|
26
|
+
export interface InstallationCli {
|
|
27
|
+
readonly provider: string;
|
|
28
|
+
readonly commands: readonly InstallationCommand[];
|
|
29
|
+
readonly usage: string;
|
|
30
|
+
run(command: InstallationCommand, argv: readonly string[]): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
export interface InstallationCliModule {
|
|
33
|
+
readonly installationCli: InstallationCli;
|
|
34
|
+
}
|
|
35
|
+
export declare const isInstallationCli: (value: unknown) => value is InstallationCli;
|
|
36
|
+
export declare const supportsInstallationCommand: (installation: InstallationCli, command: InstallationCommand) => boolean;
|
|
37
|
+
export declare const installationCliFromModule: (value: unknown, expectedProvider?: string) => InstallationCli;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** A value that can cross the provider persistence boundary without provider-specific types. */
|
|
2
|
+
export type JsonValue = null | boolean | number | string | JsonValue[] | {
|
|
3
|
+
[key: string]: JsonValue;
|
|
4
|
+
};
|
|
5
|
+
/** Persisted provider data. The provider owns the payload schema and its version. */
|
|
6
|
+
export interface ProviderEnvelope {
|
|
7
|
+
readonly provider: string;
|
|
8
|
+
readonly version: number;
|
|
9
|
+
readonly payload: JsonValue;
|
|
10
|
+
}
|
|
11
|
+
/** Converts one provider-owned value to and from its versioned JSON payload. */
|
|
12
|
+
export interface ProviderCodec<T> {
|
|
13
|
+
readonly version: number;
|
|
14
|
+
encode(value: T): JsonValue;
|
|
15
|
+
decode(payload: JsonValue): T;
|
|
16
|
+
}
|
|
17
|
+
export declare const decodeEnvelope: <T>(kind: string, provider: string, envelope: ProviderEnvelope, codec: ProviderCodec<T>) => T;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { AppSchema } from '@fabrika/auth-core';
|
|
2
|
+
/** Inputs needed to reconcile one app's authorization vocabulary into IAM. */
|
|
3
|
+
export interface SchemaReconcileInput {
|
|
4
|
+
url: string;
|
|
5
|
+
app: string;
|
|
6
|
+
schema: AppSchema;
|
|
7
|
+
/**
|
|
8
|
+
* Every origin IAM may hand this app a session at, as known by the control plane. Omitted leaves
|
|
9
|
+
* IAM's registry alone; a supplied set replaces it. It travels beside the schema and never inside
|
|
10
|
+
* it — an app declares its vocabulary, the control plane declares where the app lives.
|
|
11
|
+
*/
|
|
12
|
+
returnOrigins?: readonly string[];
|
|
13
|
+
adminKey?: string;
|
|
14
|
+
signal: AbortSignal;
|
|
15
|
+
}
|
|
16
|
+
/** Provider-neutral port for authorization schema reconciliation. */
|
|
17
|
+
export type SchemaReconciler = (input: SchemaReconcileInput) => Promise<void>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** What happened to one provider deploy step. */
|
|
2
|
+
export interface DeployStepResult {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
readonly status: 'succeeded' | 'failed' | 'skipped';
|
|
5
|
+
readonly error?: string;
|
|
6
|
+
readonly startedAt?: number;
|
|
7
|
+
readonly finishedAt?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface RunStepsOptions {
|
|
10
|
+
readonly appId: string;
|
|
11
|
+
readonly env: string;
|
|
12
|
+
readonly dryRun: boolean;
|
|
13
|
+
readonly signal: AbortSignal;
|
|
14
|
+
readonly log: (line: string) => void;
|
|
15
|
+
}
|
|
16
|
+
/** Run a provider's steps in order. The first failure or a cancellation skips every later step. */
|
|
17
|
+
export declare const runSteps: <TStep extends {
|
|
18
|
+
readonly id: string;
|
|
19
|
+
readonly description: string;
|
|
20
|
+
}>(steps: readonly TStep[], options: RunStepsOptions, runOne: (step: TStep) => Promise<void>) => Promise<{
|
|
21
|
+
readonly status: "succeeded" | "failed";
|
|
22
|
+
readonly steps: readonly DeployStepResult[];
|
|
23
|
+
}>;
|