@fabrika/provider-zerops 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.
@@ -0,0 +1,33 @@
1
+ import { ZEROPS_SHARED_POSTGRES_CONNECTION_STRING } from './namespace'
2
+ export { zeropsSharedServiceHostname, zeropsSharedServicePrefix } from './service-names'
3
+ import { APP_PROVIDER, isProviderAuthoredApp, type ProviderAuthoredApp } from '@fabrika/provider-contract'
4
+ import type { ZeropsAppConfig, ZeropsSharedPostgresBinding } from './types'
5
+
6
+ /** Declare consumption of the namespace-owned `postgres` service without resolving its credential. */
7
+ export const useSharedPostgres = (): ZeropsSharedPostgresBinding => ({
8
+ resourceKey: 'service:postgres',
9
+ hostname: 'postgres',
10
+ connectionString: ZEROPS_SHARED_POSTGRES_CONNECTION_STRING,
11
+ })
12
+
13
+ /** Provider-specific authoring identity with complete Zerops contextual typing. */
14
+ export type ZeropsAuthoredAppConfig = ZeropsAppConfig & ProviderAuthoredApp<'zerops'>
15
+
16
+ export const defineApp = (config: ZeropsAppConfig): ZeropsAuthoredAppConfig => {
17
+ if (config.id.trim() === '') {
18
+ throw new Error('defineApp: `id` is required and must be a non-empty string')
19
+ }
20
+ return { ...config, [APP_PROVIDER]: 'zerops' }
21
+ }
22
+
23
+ const property = (value: unknown, key: string): unknown => typeof value === 'object' && value !== null ? Reflect.get(value, key) : undefined
24
+
25
+ export const isZeropsAppConfig = (value: unknown): value is ZeropsAuthoredAppConfig => {
26
+ const id = property(value, 'id')
27
+ const target = property(value, 'target')
28
+ return isProviderAuthoredApp(value, 'zerops')
29
+ && typeof id === 'string'
30
+ && id.trim() !== ''
31
+ && property(target, 'platform') === 'zerops'
32
+ && typeof property(target, 'services') === 'function'
33
+ }
@@ -0,0 +1,101 @@
1
+ export interface ZeropsCliArgs {
2
+ command: string | undefined
3
+ subcommand: string | undefined
4
+ env: string | undefined
5
+ config: string
6
+ output: string
7
+ namespaceId: string | undefined
8
+ preset: string | undefined
9
+ exclusiveAppId: string | undefined
10
+ projectId: string | undefined
11
+ projectName: string | undefined
12
+ corePackage: string | undefined
13
+ publicAccess: string | undefined
14
+ postgresType: string | undefined
15
+ postgresProfile: string | undefined
16
+ proxyBuildFromGit: string | undefined
17
+ controlUrl: string | undefined
18
+ help: boolean
19
+ }
20
+
21
+ export const parseZeropsCliArgs = (argv: readonly string[]): ZeropsCliArgs => {
22
+ let command: string | undefined
23
+ let subcommand: string | undefined
24
+ let env: string | undefined
25
+ let config = './fabrika.config.ts'
26
+ let output = './fabrika.manifest.json'
27
+ let namespaceId: string | undefined
28
+ let preset: string | undefined
29
+ let exclusiveAppId: string | undefined
30
+ let projectId: string | undefined
31
+ let projectName: string | undefined
32
+ let corePackage: string | undefined
33
+ let publicAccess: string | undefined
34
+ let postgresType: string | undefined
35
+ let postgresProfile: string | undefined
36
+ let proxyBuildFromGit: string | undefined
37
+ let controlUrl: string | undefined
38
+ let help = false
39
+
40
+ for (const arg of argv) {
41
+ if (arg === '-h' || arg === '--help') {
42
+ help = true
43
+ } else if (arg.startsWith('--env=')) {
44
+ env = arg.slice('--env='.length)
45
+ } else if (arg.startsWith('--config=')) {
46
+ config = arg.slice('--config='.length)
47
+ } else if (arg.startsWith('--output=')) {
48
+ output = arg.slice('--output='.length)
49
+ } else if (arg.startsWith('--id=')) {
50
+ namespaceId = arg.slice('--id='.length)
51
+ } else if (arg.startsWith('--preset=')) {
52
+ preset = arg.slice('--preset='.length)
53
+ } else if (arg.startsWith('--exclusive-app=')) {
54
+ exclusiveAppId = arg.slice('--exclusive-app='.length)
55
+ } else if (arg.startsWith('--project-id=')) {
56
+ projectId = arg.slice('--project-id='.length)
57
+ } else if (arg.startsWith('--project-name=')) {
58
+ projectName = arg.slice('--project-name='.length)
59
+ } else if (arg.startsWith('--core-package=')) {
60
+ corePackage = arg.slice('--core-package='.length)
61
+ } else if (arg.startsWith('--public-access=')) {
62
+ publicAccess = arg.slice('--public-access='.length)
63
+ } else if (arg.startsWith('--postgres-type=')) {
64
+ postgresType = arg.slice('--postgres-type='.length)
65
+ } else if (arg.startsWith('--postgres-profile=')) {
66
+ postgresProfile = arg.slice('--postgres-profile='.length)
67
+ } else if (arg.startsWith('--proxy-build-from-git=')) {
68
+ proxyBuildFromGit = arg.slice('--proxy-build-from-git='.length)
69
+ } else if (arg.startsWith('--control-url=')) {
70
+ controlUrl = arg.slice('--control-url='.length)
71
+ } else if (arg.startsWith('-')) {
72
+ throw new Error(`Unknown option: ${arg}`)
73
+ } else if (command === undefined) {
74
+ command = arg
75
+ } else if (subcommand === undefined && command === 'namespace') {
76
+ subcommand = arg
77
+ } else {
78
+ throw new Error(`Unexpected argument: ${arg}`)
79
+ }
80
+ }
81
+
82
+ return {
83
+ command,
84
+ subcommand,
85
+ env,
86
+ config,
87
+ output,
88
+ namespaceId,
89
+ preset,
90
+ exclusiveAppId,
91
+ projectId,
92
+ projectName,
93
+ corePackage,
94
+ publicAccess,
95
+ postgresType,
96
+ postgresProfile,
97
+ proxyBuildFromGit,
98
+ controlUrl,
99
+ help,
100
+ }
101
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env bun
2
+ import { resolve } from 'node:path'
3
+ import { isZeropsAppConfig } from './authoring'
4
+ import { parseZeropsCliArgs } from './cli-args'
5
+ import { compileFabrikaManifest, manifestServiceHostnames } from './manifest'
6
+ import { runZeropsNamespaceCommand } from './namespace-command'
7
+ import type { ZeropsAppConfig } from './types'
8
+
9
+ const USAGE = `Zerops app tools
10
+
11
+ Usage:
12
+ fabrika app build --provider=zerops --env=<env> [--config=<path>] [--output=<path>]
13
+ fabrika namespace plan --provider=zerops --id=<id> --env=<env> --preset=<cheap|mid|full> [namespace options]
14
+ fabrika namespace create --provider=zerops --id=<id> --env=<env> --preset=<cheap|mid|full> [namespace options] [--control-url=<url>]
15
+ fabrika namespace adopt --provider=zerops --id=<id> --env=<env> --preset=<cheap|mid|full> --project-id=<id> [namespace options] [--control-url=<url>]
16
+ fabrika namespace reconcile --provider=zerops --id=<id> [--control-url=<url>]
17
+
18
+ Options:
19
+ --env=<env> Environment.
20
+ --config=<path> App config (default: ./fabrika.config.ts).
21
+ --output=<path> Artifact path (default: ./fabrika.manifest.json).
22
+ --id=<id> Namespace id.
23
+ --preset=<cheap|mid|full> Provider-owned namespace preset.
24
+ --exclusive-app=<id> App reserved by a full namespace.
25
+ --project-id=<id> Existing Zerops project id for adoption.
26
+ --project-name=<name> Zerops project name (default: namespace id).
27
+ --core-package=<tier> LIGHT or SERIOUS.
28
+ --public-access=<mode> custom-domain or zerops-subdomain.
29
+ --postgres-type=<type> Cheap-tier PostgreSQL type.
30
+ --postgres-profile=<profile> Cheap-tier PostgreSQL profile.
31
+ --proxy-build-from-git=<url> Public proxy source; or FABRIKA_ZEROPS_PROXY_BUILD_FROM_GIT.
32
+ --control-url=<url> Control origin; or FABRIKA_CONTROL_URL.
33
+ -h, --help Show this help.
34
+
35
+ Mutating namespace commands read the Bearer credential from FABRIKA_CONTROL_TOKEN.
36
+ `
37
+
38
+ const property = (value: unknown, key: string): unknown => typeof value === 'object' && value !== null ? Reflect.get(value, key) : undefined
39
+
40
+ const loadConfig = async (path: string): Promise<ZeropsAppConfig> => {
41
+ const absolute = resolve(process.cwd(), path)
42
+ const loaded: unknown = await import(absolute)
43
+ const config = property(loaded, 'default')
44
+ if (!isZeropsAppConfig(config)) {
45
+ throw new Error(`Config at ${absolute} must default-export defineApp({ target: { platform: 'zerops', ... } })`)
46
+ }
47
+ return config
48
+ }
49
+
50
+ export const runZeropsCli = async (argv: readonly string[]): Promise<void> => {
51
+ const args = parseZeropsCliArgs(argv)
52
+ if (args.help || args.command === undefined) {
53
+ console.info(USAGE)
54
+ return
55
+ }
56
+ if (args.command === 'namespace') {
57
+ await runZeropsNamespaceCommand(args, {
58
+ source: process.env,
59
+ fetch: globalThis.fetch,
60
+ write: (value) => console.info(value.trimEnd()),
61
+ })
62
+ return
63
+ }
64
+ if (args.command !== 'build') {
65
+ throw new Error(`Unknown command: ${args.command}\n${USAGE}`)
66
+ }
67
+ if (args.env === undefined || args.env === '') {
68
+ throw new Error('`build` requires --env=<env>')
69
+ }
70
+ const config = await loadConfig(args.config)
71
+ const manifest = compileFabrikaManifest(config, args.env)
72
+ const output = resolve(process.cwd(), args.output)
73
+ await Bun.write(output, `${JSON.stringify(manifest, null, '\t')}\n`)
74
+ console.info(`wrote ${output} (${manifestServiceHostnames(manifest).length} service(s))`)
75
+ }
76
+
77
+ if (import.meta.main) {
78
+ await runZeropsCli(process.argv.slice(2)).catch((error: unknown) => {
79
+ console.error(error instanceof Error ? error.message : String(error))
80
+ process.exit(1)
81
+ })
82
+ }
package/src/codec.ts ADDED
@@ -0,0 +1,50 @@
1
+ import type { JsonValue, ProviderCodec } from '@fabrika/provider-contract'
2
+ import type { ZeropsRuntimeTarget } from './types'
3
+
4
+ const field = (payload: JsonValue, key: string): JsonValue | undefined => {
5
+ if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) {
6
+ throw new Error('Zerops target must be an object')
7
+ }
8
+ return payload[key]
9
+ }
10
+
11
+ const requiredString = (payload: JsonValue, key: string): string => {
12
+ const value = field(payload, key)
13
+ if (typeof value !== 'string' || value === '') {
14
+ throw new Error(`Zerops target ${key} must be a non-empty string`)
15
+ }
16
+ return value
17
+ }
18
+
19
+ const optionalString = (payload: JsonValue, key: string): string | undefined => {
20
+ const value = field(payload, key)
21
+ if (value === undefined) {
22
+ return undefined
23
+ }
24
+ if (typeof value !== 'string' || value === '') {
25
+ throw new Error(`Zerops target ${key} must be a non-empty string when present`)
26
+ }
27
+ return value
28
+ }
29
+
30
+ export const zeropsTargetCodec: ProviderCodec<ZeropsRuntimeTarget> = {
31
+ version: 1,
32
+ encode: (target) => ({
33
+ projectId: target.projectId,
34
+ serviceId: target.serviceId,
35
+ accessToken: target.accessToken,
36
+ ...(target.buildFromGit !== undefined ? { buildFromGit: target.buildFromGit } : {}),
37
+ ...(target.apiBaseUrl !== undefined ? { apiBaseUrl: target.apiBaseUrl } : {}),
38
+ ...(target.propustkaUrl !== undefined ? { propustkaUrl: target.propustkaUrl } : {}),
39
+ ...(target.adminKey !== undefined ? { adminKey: target.adminKey } : {}),
40
+ }),
41
+ decode: (payload) => ({
42
+ projectId: requiredString(payload, 'projectId'),
43
+ serviceId: requiredString(payload, 'serviceId'),
44
+ accessToken: requiredString(payload, 'accessToken'),
45
+ ...(optionalString(payload, 'buildFromGit') !== undefined ? { buildFromGit: optionalString(payload, 'buildFromGit') } : {}),
46
+ ...(optionalString(payload, 'apiBaseUrl') !== undefined ? { apiBaseUrl: optionalString(payload, 'apiBaseUrl') } : {}),
47
+ ...(optionalString(payload, 'propustkaUrl') !== undefined ? { propustkaUrl: optionalString(payload, 'propustkaUrl') } : {}),
48
+ ...(optionalString(payload, 'adminKey') !== undefined ? { adminKey: optionalString(payload, 'adminKey') } : {}),
49
+ }),
50
+ }
@@ -0,0 +1,47 @@
1
+ import { reconcileSchema } from '@fabrika/auth'
2
+ import type { SchemaReconciler } from '@fabrika/provider-contract'
3
+ import { createZeropsApi, type ZeropsApi } from './api'
4
+ import type { ZeropsRuntimeTarget } from './types'
5
+
6
+ export type Sleeper = (ms: number, signal: AbortSignal) => Promise<void>
7
+
8
+ export const defaultSleep: Sleeper = (ms, signal) =>
9
+ new Promise<void>((resolve) => {
10
+ if (signal.aborted) {
11
+ resolve()
12
+ return
13
+ }
14
+ const timer = setTimeout(() => {
15
+ signal.removeEventListener('abort', onAbort)
16
+ resolve()
17
+ }, ms)
18
+ const onAbort = (): void => {
19
+ clearTimeout(timer)
20
+ resolve()
21
+ }
22
+ signal.addEventListener('abort', onAbort, { once: true })
23
+ })
24
+
25
+ export interface ZeropsCollaborators {
26
+ api: ZeropsApi
27
+ reconcileSchema: SchemaReconciler
28
+ sleep: Sleeper
29
+ }
30
+
31
+ export type ZeropsCollaboratorFactory = (target: ZeropsRuntimeTarget) => ZeropsCollaborators
32
+
33
+ const defaultReconcileSchema: SchemaReconciler = (input) =>
34
+ reconcileSchema({
35
+ url: input.url,
36
+ app: input.app,
37
+ schema: input.schema,
38
+ ...(input.returnOrigins === undefined ? {} : { returnOrigins: input.returnOrigins }),
39
+ adminKey: input.adminKey,
40
+ signal: input.signal,
41
+ })
42
+
43
+ export const defaultZeropsCollaborators: ZeropsCollaboratorFactory = (target) => ({
44
+ api: createZeropsApi({ token: target.accessToken, baseUrl: target.apiBaseUrl }),
45
+ reconcileSchema: defaultReconcileSchema,
46
+ sleep: defaultSleep,
47
+ })