@fabrika/provider-contract 0.0.26 → 0.3.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fabrika/provider-contract",
3
- "version": "0.0.26",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -19,6 +19,11 @@
19
19
  "bun": "./src/index.ts",
20
20
  "types": "./src/index.ts",
21
21
  "default": "./src/index.ts"
22
+ },
23
+ "./installation": {
24
+ "bun": "./src/installation.ts",
25
+ "types": "./src/installation.ts",
26
+ "default": "./src/installation.ts"
22
27
  }
23
28
  },
24
29
  "scripts": {
@@ -26,6 +31,6 @@
26
31
  "test": "bun test"
27
32
  },
28
33
  "dependencies": {
29
- "@fabrika/auth-core": "^0.0.26"
34
+ "@fabrika/auth-core": "^0.3.1"
30
35
  }
31
36
  }
package/src/control.ts CHANGED
@@ -1,6 +1,22 @@
1
- import type { ProviderManagedEnvironment, ProviderRunEvents } from './deploy'
2
1
  import type { JsonValue, ProviderEnvelope } from './json'
3
2
 
3
+ /** Events whose persistence and presentation belong to the control plane. */
4
+ export interface ProviderRunEvents {
5
+ log(line: string): void
6
+ /** Atomically persist the external operation id and, when supplied, its credential-free checkpoint. */
7
+ externalId(id: string, state?: JsonValue): Promise<void>
8
+ /** Replace the credential-free checkpoint while this run remains active. */
9
+ checkpoint(state: JsonValue): Promise<void>
10
+ /**
11
+ * A step boundary: Control checks its lease and reads the run's cancellation request. Rejects with a
12
+ * `ProviderInvocationAbort` after aborting the run's signal. A long step calls it at its own boundaries.
13
+ */
14
+ boundary(): Promise<void>
15
+ }
16
+
17
+ /** Complete platform-owned runtime state. A null value removes the key from persistent targets. */
18
+ export type ProviderManagedEnvironment = Readonly<Record<string, string | null>>
19
+
4
20
  /** Provider-neutral coordinates for obtaining one app revision. */
5
21
  export interface ProviderSource {
6
22
  readonly repoUrl: string
@@ -8,8 +24,6 @@ export interface ProviderSource {
8
24
  readonly workerDir?: string
9
25
  readonly buildCommand?: string
10
26
  readonly configPath?: string
11
- readonly githubConnectionId?: string
12
- readonly githubInstallationId?: number
13
27
  }
14
28
 
15
29
  /** App data needed by a control provider. */
@@ -54,6 +68,7 @@ export interface ProviderSourceResolutionInput {
54
68
  readonly environment: ProviderEnvironment
55
69
  /** An immutable commit already recorded by the trigger, which resolution must verify exactly. */
56
70
  readonly expectedCommitSha?: string
71
+ readonly sourceBinding?: ProviderSourceBinding
57
72
  readonly signal: AbortSignal
58
73
  }
59
74
 
@@ -62,6 +77,20 @@ export interface ProviderSourceResolution {
62
77
  readonly commitSha: string
63
78
  }
64
79
 
80
+ /**
81
+ * Credential-free selector of the GitHub connection and installation that read a private source
82
+ * (ADR-0029). Absent means a public source; an installation id alone never selects a credential.
83
+ */
84
+ export interface ProviderSourceBinding {
85
+ readonly connectionId: string
86
+ readonly installationId: number
87
+ }
88
+
89
+ /** How private GitHub sources reach a provider (ADR-0029): one keyed connection's App and installation. */
90
+ export interface ProviderSourceCapabilities {
91
+ readonly binding: 'connection'
92
+ }
93
+
65
94
  /** One control-plane deploy invocation. */
66
95
  export interface ProviderDeployInput {
67
96
  readonly runId: string
@@ -72,12 +101,13 @@ export interface ProviderDeployInput {
72
101
  /** Platform-owned runtime configuration, separate from operator-managed application vars. */
73
102
  readonly managedEnvironment: ProviderManagedEnvironment
74
103
  /**
75
- * Every origin IAM may hand this app a session at (ADR-0021). App-wide, not per-environment:
104
+ * Every origin IAM may hand this app a session at (ADR-0022). App-wide, not per-environment:
76
105
  * IAM's registry is keyed by app id, so the set is assembled from every environment of the app.
77
106
  * Absent when the app has no public origin at all, which leaves IAM's registry untouched.
78
107
  */
79
108
  readonly returnOrigins?: readonly string[]
80
109
  readonly dryRun: boolean
110
+ readonly sourceBinding?: ProviderSourceBinding
81
111
  /** Optional short-lived, run-scoped Operations artifact destination. */
82
112
  readonly artifactUpload?: {
83
113
  readonly url: string
@@ -100,28 +130,81 @@ export interface ProviderTerminalOutcome {
100
130
  readonly artifactState?: 'complete' | 'incomplete' | 'not_applicable'
101
131
  }
102
132
 
133
+ /** Running means external settlement is unresolved, including an ambiguous submission response. */
134
+ export type ProviderDeployOutcome = ProviderTerminalOutcome | { readonly state: 'running' }
135
+
136
+ /** Settled does not imply rollback: when activation won before cancellation, `outcome` is `succeeded`. */
137
+ export type ProviderCancelOutcome = { readonly state: 'pending' } | { readonly state: 'settled'; readonly outcome?: 'succeeded' | 'failed' }
138
+
139
+ /** Losing invocation ownership must never be interpreted as permission to cancel external work. */
140
+ export class ProviderInvocationAbort extends Error {
141
+ constructor(readonly kind: 'lease_lost' | 'cancel_requested') {
142
+ super(kind === 'lease_lost' ? 'provider invocation lost its lease' : 'provider cancellation requested')
143
+ this.name = 'ProviderInvocationAbort'
144
+ }
145
+ }
146
+
147
+ export const isLeaseLoss = (reason: unknown): boolean => reason instanceof ProviderInvocationAbort && reason.kind === 'lease_lost'
148
+
149
+ /** A signal that aborts with `signal`'s reason only when the lease is lost, never on a cancel. */
150
+ export const followLeaseLoss = (signal: AbortSignal): AbortSignal => {
151
+ const controller = new AbortController()
152
+ const follow = (): void => {
153
+ if (isLeaseLoss(signal.reason)) controller.abort(signal.reason)
154
+ }
155
+ if (signal.aborted) follow()
156
+ else signal.addEventListener('abort', follow, { once: true })
157
+ return controller.signal
158
+ }
159
+
160
+ /** Resolve after `ms`, or reject with the signal's reason once it aborts. */
161
+ export const abortableWait = (ms: number, signal: AbortSignal): Promise<void> =>
162
+ new Promise((resolve, reject) => {
163
+ signal.throwIfAborted()
164
+ const abort = (): void => {
165
+ clearTimeout(timer)
166
+ reject(signal.reason)
167
+ }
168
+ const timer = setTimeout(() => {
169
+ signal.removeEventListener('abort', abort)
170
+ resolve()
171
+ }, ms)
172
+ signal.addEventListener('abort', abort, { once: true })
173
+ })
174
+
103
175
  /** A platform-owned run that can be cancelled or reconciled. */
104
176
  export interface ProviderRunReference {
105
177
  readonly runId: string
106
178
  readonly externalId: string
107
179
  readonly environment: ProviderEnvironment
108
- /** Credential-free provider-owned progress needed to resume the external operation. */
180
+ /** Credential-free provider-owned checkpoint recorded by the run's last owner. */
109
181
  readonly providerState?: JsonValue
182
+ /** When the run started (`started_at`, else `created_at`), in epoch milliseconds; bounds how long a provider polls. */
183
+ readonly startedAtMs?: number
110
184
  }
111
185
 
112
- export type ProviderCancelInput = ProviderRunReference
186
+ export interface ProviderCancelInput extends ProviderRunReference {
187
+ readonly signal: AbortSignal
188
+ /** See `ProviderDeployInput.returnOrigins`; a cancel that settles an activated deploy projects the same set. */
189
+ readonly returnOrigins?: readonly string[]
190
+ }
113
191
 
114
192
  export interface ProviderReconcileInput extends ProviderRunReference {
115
- /** See `ProviderDeployInput.returnOrigins`; a provider that finishes a resumed deploy projects the same set. */
193
+ readonly signal: AbortSignal
194
+ /** See `ProviderDeployInput.returnOrigins`; a provider that settles a deploy here projects the same set. */
116
195
  readonly returnOrigins?: readonly string[]
117
- /** Persist a credential-free recovery checkpoint before the next irreversible provider call. */
118
- checkpoint(state: JsonValue): Promise<void>
196
+ /** Append a line to the run's log. */
197
+ log?(line: string): Promise<void>
119
198
  }
120
199
 
121
200
  /** The latest state reported while reconciling a platform-owned run. */
122
201
  export interface ProviderReconcileOutcome {
123
202
  readonly state: 'running' | 'succeeded' | 'failed'
124
203
  readonly exitCode?: number
204
+ /** See `ProviderTerminalOutcome.artifactState`. */
205
+ readonly artifactState?: 'complete' | 'incomplete' | 'not_applicable'
206
+ /** The settled run was a preview: core projects it as a dry run. */
207
+ readonly dryRun?: boolean
125
208
  }
126
209
 
127
210
  /** Write one secret value to provider-managed storage. */
@@ -164,18 +247,54 @@ export class ProviderNamespaceError extends Error {
164
247
  }
165
248
  }
166
249
 
250
+ /**
251
+ * A provider-managed secret write the provider refuses on the app's own terms — a value naming a
252
+ * service the app does not own, say. `message` carries the secret's name and the offending host,
253
+ * never the value, so core may hand it to the caller as a 4xx.
254
+ */
255
+ export class ProviderSecretError extends Error {
256
+ constructor(message: string, readonly code: string) {
257
+ super(message)
258
+ this.name = 'ProviderSecretError'
259
+ }
260
+ }
261
+
167
262
  /** Persist a canonical namespace target immediately after one external mutation succeeds. */
168
263
  export interface ProviderNamespaceEvents {
169
264
  checkpoint(namespace: ProviderDeploymentNamespace): Promise<void>
265
+ /** A step boundary: Control checks the namespace lease and, for a deploy, the run's cancellation request. */
266
+ boundary(): Promise<void>
267
+ }
268
+
269
+ /** One app environment assigned to a namespace, as its proxy routes it. */
270
+ export interface ProviderNamespaceMember {
271
+ readonly appId: string
272
+ readonly env: string
273
+ readonly domain?: string
274
+ readonly artifact: ProviderEnvelope
170
275
  }
171
276
 
172
277
  /** One idempotent namespace lifecycle invocation. */
173
278
  export interface ProviderNamespaceMutationInput {
174
279
  readonly namespace: ProviderDeploymentNamespace
280
+ /** Every app environment assigned to the namespace, read under its lease. */
281
+ readonly members: readonly ProviderNamespaceMember[]
175
282
  readonly signal: AbortSignal
176
283
  readonly events: ProviderNamespaceEvents
177
284
  }
178
285
 
286
+ /** `settled` is false while the rebuild is still in flight; core then hands it to a `reconcile` job. */
287
+ export interface ProviderNamespaceRepublishResult {
288
+ readonly namespace: ProviderDeploymentNamespace
289
+ readonly settled: boolean
290
+ }
291
+
292
+ /** Rebuild a ready namespace's entry point from its current members. */
293
+ export interface ProviderNamespaceRepublishInput extends ProviderNamespaceMutationInput {
294
+ /** `started`: return once the rebuild is submitted. `active`: return once it is live. */
295
+ readonly wait: 'started' | 'active'
296
+ }
297
+
179
298
  /** One registration whose provider coordinates do not exist until namespace services are prepared. */
180
299
  export interface ProviderRegistrationPreparationInput {
181
300
  readonly registration: ProviderRegistration
@@ -261,6 +380,8 @@ export interface ProviderNamespaceCapabilities {
261
380
  prepareRegistration?(input: ProviderRegistrationPreparationInput): Promise<ProviderRegistration>
262
381
  provision(input: ProviderNamespaceMutationInput): Promise<ProviderDeploymentNamespace>
263
382
  reconcile(input: ProviderNamespaceMutationInput): Promise<ProviderDeploymentNamespace>
383
+ /** Absent means the namespace has no entry point that follows its members. */
384
+ republish?(input: ProviderNamespaceRepublishInput): Promise<ProviderNamespaceRepublishResult>
264
385
  readonly operator?: ProviderNamespaceOperator
265
386
  }
266
387
 
@@ -272,13 +393,16 @@ export interface ProviderNamespaceCapabilities {
272
393
  */
273
394
  export interface ControlProvider {
274
395
  readonly id: string
275
- normalizeRegistration(input: ProviderRegistrationInput): ProviderRegistration
396
+ /** Absent means core stores the registration as submitted. */
397
+ normalizeRegistration?(input: ProviderRegistrationInput): ProviderRegistration
276
398
  resolveSource?(input: ProviderSourceResolutionInput): Promise<ProviderSourceResolution>
277
- deploy(input: ProviderDeployInput): Promise<ProviderTerminalOutcome>
278
- cancel?(input: ProviderCancelInput): Promise<void>
399
+ deploy(input: ProviderDeployInput): Promise<ProviderDeployOutcome>
400
+ cancel?(input: ProviderCancelInput): Promise<ProviderCancelOutcome>
279
401
  reconcile?(input: ProviderReconcileInput): Promise<ProviderReconcileOutcome>
280
402
  readonly secrets?: ProviderManagedSecrets
281
403
  readonly namespaces?: ProviderNamespaceCapabilities
404
+ /** Absent means the provider deploys public sources only. */
405
+ readonly source?: ProviderSourceCapabilities
282
406
  /**
283
407
  * The variable names this artifact declares, so core can refuse one it would silently ignore
284
408
  * ([ADR-0035](../../../docs/decisions/0035-the-platform-owns-the-application-iam-issuer.md)).
package/src/index.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  export * from './authoring'
2
2
  export * from './control'
3
- export * from './deploy'
4
3
  export * from './json'
5
- export * from './provider'
6
4
  export * from './schema'
5
+ export * from './steps'
@@ -0,0 +1,66 @@
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
+ export type InstallationCommand = 'init' | 'install' | 'plan' | 'deploy' | 'admin' | 'upgrade'
22
+
23
+ export const isInstallationCommand = (value: unknown): value is InstallationCommand =>
24
+ value === 'init' || value === 'install' || value === 'plan' || value === 'deploy' || value === 'admin' || value === 'upgrade'
25
+
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
+
33
+ export interface InstallationCliModule {
34
+ readonly installationCli: InstallationCli
35
+ }
36
+
37
+ export const isInstallationCli = (value: unknown): value is InstallationCli =>
38
+ typeof value === 'object'
39
+ && value !== null
40
+ && 'provider' in value
41
+ && typeof value.provider === 'string'
42
+ && value.provider.trim() !== ''
43
+ && 'commands' in value
44
+ && Array.isArray(value.commands)
45
+ && value.commands.length > 0
46
+ && value.commands.every(isInstallationCommand)
47
+ && new Set(value.commands).size === value.commands.length
48
+ && 'usage' in value
49
+ && typeof value.usage === 'string'
50
+ && 'run' in value
51
+ && typeof value.run === 'function'
52
+
53
+ export const supportsInstallationCommand = (installation: InstallationCli, command: InstallationCommand): boolean =>
54
+ installation.commands.some((supported) => supported === command)
55
+
56
+ export const installationCliFromModule = (value: unknown, expectedProvider?: string): InstallationCli => {
57
+ if (typeof value !== 'object' || value === null || !('installationCli' in value) || !isInstallationCli(value.installationCli)) {
58
+ throw new Error('Installation package must export a valid `installationCli`')
59
+ }
60
+ if (expectedProvider !== undefined && value.installationCli.provider !== expectedProvider) {
61
+ throw new Error(
62
+ `Installation package provides "${value.installationCli.provider}", expected "${expectedProvider}"`,
63
+ )
64
+ }
65
+ return value.installationCli
66
+ }
package/src/json.ts CHANGED
@@ -14,3 +14,13 @@ export interface ProviderCodec<T> {
14
14
  encode(value: T): JsonValue
15
15
  decode(payload: JsonValue): T
16
16
  }
17
+
18
+ export const decodeEnvelope = <T>(kind: string, provider: string, envelope: ProviderEnvelope, codec: ProviderCodec<T>): T => {
19
+ if (envelope.provider !== provider) {
20
+ throw new Error(`${kind} belongs to provider "${envelope.provider}", expected "${provider}"`)
21
+ }
22
+ if (envelope.version !== codec.version) {
23
+ throw new Error(`${kind} schema version ${envelope.version} is not supported by provider "${provider}"`)
24
+ }
25
+ return codec.decode(envelope.payload)
26
+ }
package/src/steps.ts ADDED
@@ -0,0 +1,72 @@
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
+
10
+ export interface RunStepsOptions {
11
+ readonly appId: string
12
+ readonly env: string
13
+ readonly dryRun: boolean
14
+ readonly signal: AbortSignal
15
+ readonly log: (line: string) => void
16
+ }
17
+
18
+ const CANCELLED = 'deploy cancelled'
19
+
20
+ /** Stop waiting as soon as the run is cancelled; the step receives the same signal and stops its own work. */
21
+ const untilCancelled = <T>(promise: Promise<T>, signal: AbortSignal): Promise<T> =>
22
+ new Promise<T>((resolve, reject) => {
23
+ const onAbort = (): void => reject(new Error(CANCELLED))
24
+ if (signal.aborted) onAbort()
25
+ else signal.addEventListener('abort', onAbort, { once: true })
26
+ // Always observed, so a step that fails after it was abandoned is not an unhandled rejection.
27
+ void promise.then(resolve, reject).finally(() => {
28
+ signal.removeEventListener('abort', onAbort)
29
+ })
30
+ })
31
+
32
+ /** Run a provider's steps in order. The first failure or a cancellation skips every later step. */
33
+ export const runSteps = async <TStep extends { readonly id: string; readonly description: string }>(
34
+ steps: readonly TStep[],
35
+ options: RunStepsOptions,
36
+ runOne: (step: TStep) => Promise<void>,
37
+ ): Promise<{ readonly status: 'succeeded' | 'failed'; readonly steps: readonly DeployStepResult[] }> => {
38
+ const { log, signal } = options
39
+ log(`Deploy ${options.appId} → ${options.env}${options.dryRun ? ' (dry-run)' : ''} — ${steps.length} step(s):`)
40
+ for (const step of steps) {
41
+ log(` • ${step.id} — ${step.description}`)
42
+ }
43
+
44
+ const results: DeployStepResult[] = []
45
+ let stopped = false
46
+ for (const step of steps) {
47
+ if (stopped) {
48
+ results.push({ id: step.id, status: 'skipped' })
49
+ continue
50
+ }
51
+ if (signal.aborted) {
52
+ stopped = true
53
+ results.push({ id: step.id, status: 'skipped' })
54
+ log(`∅ ${step.id}: ${CANCELLED}`)
55
+ continue
56
+ }
57
+
58
+ const startedAt = Date.now()
59
+ log(`→ ${step.id}`)
60
+ try {
61
+ await untilCancelled(runOne(step), signal)
62
+ results.push({ id: step.id, status: 'succeeded', startedAt, finishedAt: Date.now() })
63
+ } catch (error) {
64
+ const message = error instanceof Error ? error.message : String(error)
65
+ stopped = true
66
+ log(`✗ ${step.id}: ${message}`)
67
+ results.push({ id: step.id, status: 'failed', error: message, startedAt, finishedAt: Date.now() })
68
+ }
69
+ }
70
+
71
+ return { status: stopped ? 'failed' : 'succeeded', steps: results }
72
+ }
package/src/deploy.ts DELETED
@@ -1,88 +0,0 @@
1
- import type { JsonValue, ProviderEnvelope } from './json'
2
-
3
- /** One provider-owned unit of deploy work. Core treats `kind` as an open string. */
4
- export interface ProviderJobSpec {
5
- readonly id: string
6
- readonly kind: string
7
- readonly description: string
8
- readonly dependsOn?: readonly string[]
9
- }
10
-
11
- /** Shared deploy state used for whole runs and individual steps. */
12
- export type ProviderRunStatus = 'pending' | 'running' | 'succeeded' | 'failed' | 'skipped'
13
-
14
- /** A provider job plus its observed runtime state. */
15
- export interface ProviderDeployStep {
16
- readonly spec: ProviderJobSpec
17
- readonly status: ProviderRunStatus
18
- readonly error?: string
19
- readonly startedAt?: number
20
- readonly finishedAt?: number
21
- }
22
-
23
- /** The ordered work derived by a provider before it starts mutation. */
24
- export interface ProviderDeployPlan {
25
- readonly appId: string
26
- readonly env: string
27
- readonly steps: readonly ProviderJobSpec[]
28
- }
29
-
30
- /** A provider-owned deploy session opened from one target and artifact. */
31
- export interface ProviderDeploySession {
32
- readonly plan: ProviderDeployPlan
33
- execute(stepId: string): Promise<void>
34
- }
35
-
36
- /** The final provider-neutral representation of a deploy execution. */
37
- export interface ProviderDeployResult {
38
- readonly appId: string
39
- readonly env: string
40
- readonly status: ProviderRunStatus
41
- readonly plan: ProviderDeployPlan
42
- readonly steps: readonly ProviderDeployStep[]
43
- }
44
-
45
- /** Events whose persistence and presentation belong to the control plane. */
46
- export interface ProviderRunEvents {
47
- log(line: string): void
48
- /** Atomically persist the external operation id and, when supplied, its credential-free resume state. */
49
- externalId(id: string, state?: JsonValue): Promise<void>
50
- /** Replace the credential-free resume state while this run remains active. */
51
- checkpoint(state: JsonValue): Promise<void>
52
- }
53
-
54
- /** Complete platform-owned runtime state. A null value removes the key from persistent targets. */
55
- export type ProviderManagedEnvironment = Readonly<Record<string, string | null>>
56
-
57
- /** Universal inputs shared by every provider deploy. */
58
- export interface ProviderRunBase {
59
- readonly appId: string
60
- readonly env: string
61
- readonly domain?: string
62
- /**
63
- * Every origin IAM may hand this app a session at, as known by the control plane. App-wide rather
64
- * than per-environment, because IAM's registry is keyed by app id and a deploy of one environment
65
- * must not un-register the others.
66
- */
67
- readonly returnOrigins?: readonly string[]
68
- readonly cwd: string
69
- readonly secrets: Readonly<Record<string, string>>
70
- readonly vars: Readonly<Record<string, string>>
71
- /** Platform-owned runtime configuration, separate from operator-managed application vars. */
72
- readonly managedEnvironment: ProviderManagedEnvironment
73
- readonly dryRun: boolean
74
- readonly signal: AbortSignal
75
- readonly events: ProviderRunEvents
76
- }
77
-
78
- /** The run shape seen by a provider after its envelopes have been decoded. */
79
- export interface TypedProviderRun<TTarget, TArtifact> extends ProviderRunBase {
80
- readonly target: TTarget
81
- readonly artifact: TArtifact
82
- }
83
-
84
- /** The provider-neutral run shape stored and passed by the control plane. */
85
- export interface RuntimeProviderRun extends ProviderRunBase {
86
- readonly target: ProviderEnvelope
87
- readonly artifact: ProviderEnvelope
88
- }
package/src/provider.ts DELETED
@@ -1,78 +0,0 @@
1
- import type { ProviderDeploySession, RuntimeProviderRun, TypedProviderRun } from './deploy'
2
- import type { ProviderCodec, ProviderEnvelope } from './json'
3
-
4
- /** A provider implementation with its target and artifact types preserved. */
5
- export interface ProviderDefinition<TId extends string, TTarget, TArtifact> {
6
- readonly id: TId
7
- readonly target: ProviderCodec<TTarget>
8
- readonly artifact: ProviderCodec<TArtifact>
9
- open(run: TypedProviderRun<TTarget, TArtifact>): Promise<ProviderDeploySession>
10
- }
11
-
12
- /** The opaque interface consumed by a provider-specific composition root. */
13
- export interface RuntimeProvider {
14
- readonly id: string
15
- open(run: RuntimeProviderRun): Promise<ProviderDeploySession>
16
- }
17
-
18
- /**
19
- * A provider module exposes typed encoders to its authoring side and an opaque runtime adapter to
20
- * its control-plane side.
21
- */
22
- export interface ProviderModule<TId extends string, TTarget, TArtifact> {
23
- readonly id: TId
24
- encodeTarget(value: TTarget): ProviderEnvelope
25
- encodeArtifact(value: TArtifact): ProviderEnvelope
26
- readonly runtime: RuntimeProvider
27
- }
28
-
29
- const decodeEnvelope = <T>(kind: string, provider: string, envelope: ProviderEnvelope, codec: ProviderCodec<T>): T => {
30
- if (envelope.provider !== provider) {
31
- throw new Error(`${kind} belongs to provider "${envelope.provider}", expected "${provider}"`)
32
- }
33
- if (envelope.version !== codec.version) {
34
- throw new Error(`${kind} schema version ${envelope.version} is not supported by provider "${provider}"`)
35
- }
36
- return codec.decode(envelope.payload)
37
- }
38
-
39
- /**
40
- * Closes over a provider's concrete types and exposes only JSON envelopes at runtime.
41
- *
42
- * A new provider calls this factory in its own package. No central platform map or discriminated
43
- * union changes when a provider id is added.
44
- */
45
- export const createProvider = <TId extends string, TTarget, TArtifact>(
46
- definition: ProviderDefinition<TId, TTarget, TArtifact>,
47
- ): ProviderModule<TId, TTarget, TArtifact> => {
48
- const envelope = <T>(codec: ProviderCodec<T>, value: T): ProviderEnvelope => ({
49
- provider: definition.id,
50
- version: codec.version,
51
- payload: codec.encode(value),
52
- })
53
-
54
- return {
55
- id: definition.id,
56
- encodeTarget: (value) => envelope(definition.target, value),
57
- encodeArtifact: (value) => envelope(definition.artifact, value),
58
- runtime: {
59
- id: definition.id,
60
- open: async (run) =>
61
- definition.open({
62
- appId: run.appId,
63
- env: run.env,
64
- domain: run.domain,
65
- returnOrigins: run.returnOrigins,
66
- cwd: run.cwd,
67
- secrets: run.secrets,
68
- vars: run.vars,
69
- managedEnvironment: run.managedEnvironment,
70
- dryRun: run.dryRun,
71
- signal: run.signal,
72
- events: run.events,
73
- target: decodeEnvelope('target', definition.id, run.target, definition.target),
74
- artifact: decodeEnvelope('artifact', definition.id, run.artifact, definition.artifact),
75
- }),
76
- },
77
- }
78
- }