@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 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
+ }
@@ -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
@@ -0,0 +1,6 @@
1
+ export * from './authoring'
2
+ export * from './control'
3
+ export * from './deploy'
4
+ export * from './json'
5
+ export * from './provider'
6
+ export * from './schema'
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
+ }
@@ -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>