@fabrika/provider-contract 0.0.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 +31 -0
- package/src/authoring.ts +42 -0
- package/src/control.ts +224 -0
- package/src/deploy.ts +85 -0
- package/src/index.ts +6 -0
- package/src/json.ts +16 -0
- package/src/provider.ts +78 -0
- package/src/schema.ts +19 -0
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fabrika/provider-contract",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/contember/fabrika-platform.git",
|
|
8
|
+
"directory": "packages/provider-contract"
|
|
9
|
+
},
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"src",
|
|
15
|
+
"!src/**/__tests__/**"
|
|
16
|
+
],
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"bun": "./src/index.ts",
|
|
20
|
+
"types": "./src/index.ts",
|
|
21
|
+
"default": "./src/index.ts"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
26
|
+
"test": "bun test"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@fabrika/auth-core": "^0.0.1"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/authoring.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { AppSchema } from '@fabrika/auth-core'
|
|
2
|
+
|
|
3
|
+
export const APP_PROVIDER = Symbol.for('@fabrika/provider-contract/app-provider')
|
|
4
|
+
|
|
5
|
+
export interface ProviderAuthoredApp<Provider extends string = string> {
|
|
6
|
+
readonly [APP_PROVIDER]: Provider
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const authoredAppProvider = (value: unknown): string | undefined => {
|
|
10
|
+
if (typeof value !== 'object' || value === null) {
|
|
11
|
+
return undefined
|
|
12
|
+
}
|
|
13
|
+
const provider = Reflect.get(value, APP_PROVIDER)
|
|
14
|
+
return typeof provider === 'string' && provider !== '' ? provider : undefined
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const isProviderAuthoredApp = <Provider extends string>(
|
|
18
|
+
value: unknown,
|
|
19
|
+
provider: Provider,
|
|
20
|
+
): value is ProviderAuthoredApp<Provider> => authoredAppProvider(value) === provider
|
|
21
|
+
|
|
22
|
+
/** Provider-neutral build inputs and environment requirements declared by an app. */
|
|
23
|
+
export interface AppPipeline {
|
|
24
|
+
/** Source directory relative to the app config file. Defaults to `.`. */
|
|
25
|
+
workerDir?: string
|
|
26
|
+
/** Shell command that builds the deployable artifact. */
|
|
27
|
+
build?: string
|
|
28
|
+
/** Secret names that must resolve before deployment. */
|
|
29
|
+
secrets?: string[]
|
|
30
|
+
/** Non-secret variable names that must resolve before deployment. */
|
|
31
|
+
vars?: string[]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** App metadata shared by every provider-specific authoring config. */
|
|
35
|
+
export interface AppConfigBase {
|
|
36
|
+
/** Stable app id, unique within one control plane. */
|
|
37
|
+
id: string
|
|
38
|
+
/** The app-owned authorization vocabulary reconciled into IAM. */
|
|
39
|
+
schema?: AppSchema
|
|
40
|
+
/** Provider-neutral build and environment requirements. */
|
|
41
|
+
pipeline?: AppPipeline
|
|
42
|
+
}
|
package/src/control.ts
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import type { ProviderManagedEnvironment, ProviderRunEvents } from './deploy'
|
|
2
|
+
import type { JsonValue, ProviderEnvelope } from './json'
|
|
3
|
+
|
|
4
|
+
/** Provider-neutral coordinates for obtaining one app revision. */
|
|
5
|
+
export interface ProviderSource {
|
|
6
|
+
readonly repoUrl: string
|
|
7
|
+
readonly ref: string
|
|
8
|
+
readonly workerDir?: string
|
|
9
|
+
readonly buildCommand?: string
|
|
10
|
+
readonly configPath?: string
|
|
11
|
+
readonly githubInstallationId?: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** App data needed by a control provider. */
|
|
15
|
+
export interface ProviderApp {
|
|
16
|
+
readonly id: string
|
|
17
|
+
readonly source: ProviderSource
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** One provider-owned placement boundary shared by zero or more app environments. */
|
|
21
|
+
export interface ProviderDeploymentNamespace {
|
|
22
|
+
readonly id: string
|
|
23
|
+
readonly env: string
|
|
24
|
+
readonly exclusiveAppId?: string
|
|
25
|
+
readonly target: ProviderEnvelope
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Persisted deploy data for one app environment. */
|
|
29
|
+
export interface ProviderEnvironment {
|
|
30
|
+
readonly appId: string
|
|
31
|
+
readonly env: string
|
|
32
|
+
readonly domain?: string
|
|
33
|
+
/** Canonical externally reachable HTTP(S) origin; independent of provider domain routing. */
|
|
34
|
+
readonly publicOrigin?: string
|
|
35
|
+
readonly namespace?: ProviderDeploymentNamespace
|
|
36
|
+
readonly target: ProviderEnvelope
|
|
37
|
+
readonly artifact: ProviderEnvelope
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Raw registration data passed to provider-owned validation and normalization. */
|
|
41
|
+
export interface ProviderRegistrationInput {
|
|
42
|
+
readonly app: ProviderApp
|
|
43
|
+
readonly environment: ProviderEnvironment
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Canonical registration returned after provider-owned validation and normalization. */
|
|
47
|
+
export type ProviderRegistration = ProviderRegistrationInput
|
|
48
|
+
|
|
49
|
+
/** One control-plane deploy invocation. */
|
|
50
|
+
export interface ProviderDeployInput {
|
|
51
|
+
readonly runId: string
|
|
52
|
+
readonly app: ProviderApp
|
|
53
|
+
readonly environment: ProviderEnvironment
|
|
54
|
+
readonly secrets: Readonly<Record<string, string>>
|
|
55
|
+
readonly vars: Readonly<Record<string, string>>
|
|
56
|
+
/** Platform-owned runtime configuration, separate from operator-managed application vars. */
|
|
57
|
+
readonly managedEnvironment: ProviderManagedEnvironment
|
|
58
|
+
/**
|
|
59
|
+
* Every origin IAM may hand this app a session at (ADR-0021). App-wide, not per-environment:
|
|
60
|
+
* IAM's registry is keyed by app id, so the set is assembled from every environment of the app.
|
|
61
|
+
* Absent when the app has no public origin at all, which leaves IAM's registry untouched.
|
|
62
|
+
*/
|
|
63
|
+
readonly returnOrigins?: readonly string[]
|
|
64
|
+
readonly dryRun: boolean
|
|
65
|
+
/** Optional short-lived, run-scoped Operations artifact destination. */
|
|
66
|
+
readonly artifactUpload?: {
|
|
67
|
+
readonly url: string
|
|
68
|
+
readonly bearer: string
|
|
69
|
+
readonly appId: string
|
|
70
|
+
readonly environment: string
|
|
71
|
+
readonly serviceKey: string
|
|
72
|
+
readonly release: string
|
|
73
|
+
readonly runId: string
|
|
74
|
+
}
|
|
75
|
+
readonly signal: AbortSignal
|
|
76
|
+
readonly events: ProviderRunEvents
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** A terminal provider operation result. */
|
|
80
|
+
export interface ProviderTerminalOutcome {
|
|
81
|
+
readonly state: 'succeeded' | 'failed'
|
|
82
|
+
readonly exitCode?: number
|
|
83
|
+
/** Whether provider-local release artifacts reached Operations. */
|
|
84
|
+
readonly artifactState?: 'complete' | 'incomplete' | 'not_applicable'
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** A platform-owned run that can be cancelled or reconciled. */
|
|
88
|
+
export interface ProviderRunReference {
|
|
89
|
+
readonly runId: string
|
|
90
|
+
readonly externalId: string
|
|
91
|
+
readonly environment: ProviderEnvironment
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export type ProviderCancelInput = ProviderRunReference
|
|
95
|
+
|
|
96
|
+
export interface ProviderReconcileInput extends ProviderRunReference {
|
|
97
|
+
/** See `ProviderDeployInput.returnOrigins`; a provider that finishes a resumed deploy projects the same set. */
|
|
98
|
+
readonly returnOrigins?: readonly string[]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The latest state reported while reconciling a platform-owned run. */
|
|
102
|
+
export interface ProviderReconcileOutcome {
|
|
103
|
+
readonly state: 'running' | 'succeeded' | 'failed'
|
|
104
|
+
readonly exitCode?: number
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Write one secret value to provider-managed storage. */
|
|
108
|
+
export interface ProviderSecretPutInput {
|
|
109
|
+
readonly environment: ProviderEnvironment
|
|
110
|
+
readonly name: string
|
|
111
|
+
readonly value: string
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Remove one secret value from provider-managed storage. */
|
|
115
|
+
export interface ProviderSecretDeleteInput {
|
|
116
|
+
readonly environment: ProviderEnvironment
|
|
117
|
+
readonly name: string
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Opaque reference persisted by core instead of the provider-managed secret value. */
|
|
121
|
+
export interface ProviderSecretPutResult {
|
|
122
|
+
readonly valueRef: string
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Optional edit-time secret storage owned by a provider. */
|
|
126
|
+
export interface ProviderManagedSecrets {
|
|
127
|
+
put(input: ProviderSecretPutInput): Promise<ProviderSecretPutResult>
|
|
128
|
+
delete(input: ProviderSecretDeleteInput): Promise<void>
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Persist a canonical namespace target immediately after one external mutation succeeds. */
|
|
132
|
+
export interface ProviderNamespaceEvents {
|
|
133
|
+
checkpoint(namespace: ProviderDeploymentNamespace): Promise<void>
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** One idempotent namespace lifecycle invocation. */
|
|
137
|
+
export interface ProviderNamespaceMutationInput {
|
|
138
|
+
readonly namespace: ProviderDeploymentNamespace
|
|
139
|
+
readonly signal: AbortSignal
|
|
140
|
+
readonly events: ProviderNamespaceEvents
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** One registration whose provider coordinates do not exist until namespace services are prepared. */
|
|
144
|
+
export interface ProviderRegistrationPreparationInput {
|
|
145
|
+
readonly registration: ProviderRegistration
|
|
146
|
+
readonly signal: AbortSignal
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** One provider-defined operator choice. Core does not interpret its id or description. */
|
|
150
|
+
export interface ProviderNamespacePreset {
|
|
151
|
+
readonly id: string
|
|
152
|
+
readonly label: string
|
|
153
|
+
readonly description: string
|
|
154
|
+
readonly requiresExclusiveApp: boolean
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Provider-neutral coordinates plus opaque provider options for a namespace preview. */
|
|
158
|
+
export interface ProviderNamespacePlanInput {
|
|
159
|
+
readonly id: string
|
|
160
|
+
readonly env: string
|
|
161
|
+
readonly preset: string
|
|
162
|
+
readonly exclusiveAppId?: string
|
|
163
|
+
readonly options?: JsonValue
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** One safe, human-readable provider fact. Values must never contain credentials. */
|
|
167
|
+
export interface ProviderNamespaceFact {
|
|
168
|
+
readonly label: string
|
|
169
|
+
readonly value: string
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Provider-owned operator copy for a planned or persisted namespace. */
|
|
173
|
+
export interface ProviderNamespacePresentation {
|
|
174
|
+
readonly preset: string
|
|
175
|
+
readonly title: string
|
|
176
|
+
readonly facts: readonly ProviderNamespaceFact[]
|
|
177
|
+
readonly instructions: readonly string[]
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** A mutation-free preview that can be submitted unchanged to namespace creation. */
|
|
181
|
+
export interface ProviderNamespacePlan {
|
|
182
|
+
readonly namespace: ProviderDeploymentNamespace
|
|
183
|
+
readonly presentation: ProviderNamespacePresentation
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Optional operator surface for providers that offer opinionated namespace presets. */
|
|
187
|
+
export interface ProviderNamespaceOperator {
|
|
188
|
+
readonly presets: readonly ProviderNamespacePreset[]
|
|
189
|
+
plan(input: ProviderNamespacePlanInput): ProviderNamespacePlan
|
|
190
|
+
present(namespace: ProviderDeploymentNamespace): ProviderNamespacePresentation
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Optional placement lifecycle owned by providers that use deployment namespaces. */
|
|
194
|
+
export interface ProviderNamespaceCapabilities {
|
|
195
|
+
normalize(namespace: ProviderDeploymentNamespace): ProviderDeploymentNamespace
|
|
196
|
+
/** Namespace-owned resources that must be reserved before provisioning starts. */
|
|
197
|
+
namespaceResourceClaims(namespace: ProviderDeploymentNamespace): readonly string[]
|
|
198
|
+
/** App-owned resources derived from one canonical normalized registration. */
|
|
199
|
+
registrationResourceClaims(registration: ProviderRegistration): readonly string[]
|
|
200
|
+
/**
|
|
201
|
+
* Materialize app-owned provider resources and return their canonical coordinates.
|
|
202
|
+
* Core acquires every registration resource claim before invoking this mutation.
|
|
203
|
+
*/
|
|
204
|
+
prepareRegistration?(input: ProviderRegistrationPreparationInput): Promise<ProviderRegistration>
|
|
205
|
+
provision(input: ProviderNamespaceMutationInput): Promise<ProviderDeploymentNamespace>
|
|
206
|
+
reconcile(input: ProviderNamespaceMutationInput): Promise<ProviderDeploymentNamespace>
|
|
207
|
+
readonly operator?: ProviderNamespaceOperator
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* The control-plane capability bundle selected statically by one composition root.
|
|
212
|
+
*
|
|
213
|
+
* Core owns no provider registry. The selected provider validates registration envelopes and owns
|
|
214
|
+
* every provider-specific deploy lifecycle capability.
|
|
215
|
+
*/
|
|
216
|
+
export interface ControlProvider {
|
|
217
|
+
readonly id: string
|
|
218
|
+
normalizeRegistration(input: ProviderRegistrationInput): ProviderRegistration
|
|
219
|
+
deploy(input: ProviderDeployInput): Promise<ProviderTerminalOutcome>
|
|
220
|
+
cancel?(input: ProviderCancelInput): Promise<void>
|
|
221
|
+
reconcile?(input: ProviderReconcileInput): Promise<ProviderReconcileOutcome>
|
|
222
|
+
readonly secrets?: ProviderManagedSecrets
|
|
223
|
+
readonly namespaces?: ProviderNamespaceCapabilities
|
|
224
|
+
}
|
package/src/deploy.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { 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
|
+
externalId(id: string): Promise<void>
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Complete platform-owned runtime state. A null value removes the key from persistent targets. */
|
|
52
|
+
export type ProviderManagedEnvironment = Readonly<Record<string, string | null>>
|
|
53
|
+
|
|
54
|
+
/** Universal inputs shared by every provider deploy. */
|
|
55
|
+
export interface ProviderRunBase {
|
|
56
|
+
readonly appId: string
|
|
57
|
+
readonly env: string
|
|
58
|
+
readonly domain?: string
|
|
59
|
+
/**
|
|
60
|
+
* Every origin IAM may hand this app a session at, as known by the control plane. App-wide rather
|
|
61
|
+
* than per-environment, because IAM's registry is keyed by app id and a deploy of one environment
|
|
62
|
+
* must not un-register the others.
|
|
63
|
+
*/
|
|
64
|
+
readonly returnOrigins?: readonly string[]
|
|
65
|
+
readonly cwd: string
|
|
66
|
+
readonly secrets: Readonly<Record<string, string>>
|
|
67
|
+
readonly vars: Readonly<Record<string, string>>
|
|
68
|
+
/** Platform-owned runtime configuration, separate from operator-managed application vars. */
|
|
69
|
+
readonly managedEnvironment: ProviderManagedEnvironment
|
|
70
|
+
readonly dryRun: boolean
|
|
71
|
+
readonly signal: AbortSignal
|
|
72
|
+
readonly events: ProviderRunEvents
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The run shape seen by a provider after its envelopes have been decoded. */
|
|
76
|
+
export interface TypedProviderRun<TTarget, TArtifact> extends ProviderRunBase {
|
|
77
|
+
readonly target: TTarget
|
|
78
|
+
readonly artifact: TArtifact
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The provider-neutral run shape stored and passed by the control plane. */
|
|
82
|
+
export interface RuntimeProviderRun extends ProviderRunBase {
|
|
83
|
+
readonly target: ProviderEnvelope
|
|
84
|
+
readonly artifact: ProviderEnvelope
|
|
85
|
+
}
|
package/src/index.ts
ADDED
package/src/json.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** A value that can cross the provider persistence boundary without provider-specific types. */
|
|
2
|
+
export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }
|
|
3
|
+
|
|
4
|
+
/** Persisted provider data. The provider owns the payload schema and its version. */
|
|
5
|
+
export interface ProviderEnvelope {
|
|
6
|
+
readonly provider: string
|
|
7
|
+
readonly version: number
|
|
8
|
+
readonly payload: JsonValue
|
|
9
|
+
}
|
|
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
|
+
}
|
package/src/provider.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
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
|
+
}
|
package/src/schema.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { AppSchema } from '@fabrika/auth-core'
|
|
2
|
+
|
|
3
|
+
/** Inputs needed to reconcile one app's authorization vocabulary into IAM. */
|
|
4
|
+
export interface SchemaReconcileInput {
|
|
5
|
+
url: string
|
|
6
|
+
app: string
|
|
7
|
+
schema: AppSchema
|
|
8
|
+
/**
|
|
9
|
+
* Every origin IAM may hand this app a session at, as known by the control plane. Omitted leaves
|
|
10
|
+
* IAM's registry alone; a supplied set replaces it. It travels beside the schema and never inside
|
|
11
|
+
* it — an app declares its vocabulary, the control plane declares where the app lives.
|
|
12
|
+
*/
|
|
13
|
+
returnOrigins?: readonly string[]
|
|
14
|
+
adminKey?: string
|
|
15
|
+
signal: AbortSignal
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Provider-neutral port for authorization schema reconciliation. */
|
|
19
|
+
export type SchemaReconciler = (input: SchemaReconcileInput) => Promise<void>
|