@ankhorage/infra 0.0.0

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.
Files changed (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +17 -0
  3. package/dist/adapters/minikube/auth/index.d.ts +7 -0
  4. package/dist/adapters/minikube/auth/index.d.ts.map +1 -0
  5. package/dist/adapters/minikube/auth/index.js +14 -0
  6. package/dist/adapters/minikube/auth/index.js.map +1 -0
  7. package/dist/adapters/minikube/auth/supabase/index.d.ts +7 -0
  8. package/dist/adapters/minikube/auth/supabase/index.d.ts.map +1 -0
  9. package/dist/adapters/minikube/auth/supabase/index.js +213 -0
  10. package/dist/adapters/minikube/auth/supabase/index.js.map +1 -0
  11. package/dist/adapters/minikube/authz/cerbos/index.d.ts +9 -0
  12. package/dist/adapters/minikube/authz/cerbos/index.d.ts.map +1 -0
  13. package/dist/adapters/minikube/authz/cerbos/index.js +360 -0
  14. package/dist/adapters/minikube/authz/cerbos/index.js.map +1 -0
  15. package/dist/adapters/minikube/authz/index.d.ts +9 -0
  16. package/dist/adapters/minikube/authz/index.d.ts.map +1 -0
  17. package/dist/adapters/minikube/authz/index.js +23 -0
  18. package/dist/adapters/minikube/authz/index.js.map +1 -0
  19. package/dist/adapters/minikube/base/index.d.ts +9 -0
  20. package/dist/adapters/minikube/base/index.d.ts.map +1 -0
  21. package/dist/adapters/minikube/base/index.js +1371 -0
  22. package/dist/adapters/minikube/base/index.js.map +1 -0
  23. package/dist/adapters/minikube/contracts.d.ts +9 -0
  24. package/dist/adapters/minikube/contracts.d.ts.map +1 -0
  25. package/dist/adapters/minikube/contracts.js +9 -0
  26. package/dist/adapters/minikube/contracts.js.map +1 -0
  27. package/dist/adapters/minikube/index.d.ts +3 -0
  28. package/dist/adapters/minikube/index.d.ts.map +1 -0
  29. package/dist/adapters/minikube/index.js +114 -0
  30. package/dist/adapters/minikube/index.js.map +1 -0
  31. package/dist/adapters/minikube/storage/index.d.ts +7 -0
  32. package/dist/adapters/minikube/storage/index.d.ts.map +1 -0
  33. package/dist/adapters/minikube/storage/index.js +77 -0
  34. package/dist/adapters/minikube/storage/index.js.map +1 -0
  35. package/dist/adapters/minikube/storage/supabase/index.d.ts +7 -0
  36. package/dist/adapters/minikube/storage/supabase/index.d.ts.map +1 -0
  37. package/dist/adapters/minikube/storage/supabase/index.js +81 -0
  38. package/dist/adapters/minikube/storage/supabase/index.js.map +1 -0
  39. package/dist/datasets.d.ts +11 -0
  40. package/dist/datasets.d.ts.map +1 -0
  41. package/dist/datasets.js +397 -0
  42. package/dist/datasets.js.map +1 -0
  43. package/dist/index.d.ts +7 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +15 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/types.d.ts +70 -0
  48. package/dist/types.d.ts.map +1 -0
  49. package/dist/types.js +2 -0
  50. package/dist/types.js.map +1 -0
  51. package/package.json +86 -0
  52. package/src/adapters/minikube/README.md +73 -0
  53. package/src/adapters/minikube/auth/index.ts +23 -0
  54. package/src/adapters/minikube/auth/supabase/index.ts +269 -0
  55. package/src/adapters/minikube/authz/cerbos/index.ts +458 -0
  56. package/src/adapters/minikube/authz/index.ts +32 -0
  57. package/src/adapters/minikube/base/index.test.ts +103 -0
  58. package/src/adapters/minikube/base/index.ts +1478 -0
  59. package/src/adapters/minikube/contracts.ts +17 -0
  60. package/src/adapters/minikube/index.ts +149 -0
  61. package/src/adapters/minikube/storage/index.ts +91 -0
  62. package/src/adapters/minikube/storage/supabase/index.ts +112 -0
  63. package/src/datasets.test.ts +178 -0
  64. package/src/datasets.ts +562 -0
  65. package/src/index.test.ts +679 -0
  66. package/src/index.ts +43 -0
  67. package/src/minikubeImageLifecycle.test.ts +34 -0
  68. package/src/types.ts +82 -0
package/package.json ADDED
@@ -0,0 +1,86 @@
1
+ {
2
+ "name": "@ankhorage/infra",
3
+ "type": "module",
4
+ "version": "0.0.0",
5
+ "description": "Provider-neutral infrastructure generation: turns app manifests, datasets, auth, state, storage, and deployment config into generated infra artifacts, migrations, seeds, APIs, and runtime dependencies.",
6
+ "homepage": "https://github.com/ankhorage/infra#readme",
7
+ "bugs": {
8
+ "url": "https://github.com/ankhorage/infra/issues"
9
+ },
10
+ "license": "MIT",
11
+ "main": "./dist/index.js",
12
+ "sideEffects": false,
13
+ "types": "./dist/index.d.ts",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/ankhorage/infra.git"
17
+ },
18
+ "keywords": [
19
+ "ankhorage",
20
+ "infra",
21
+ "infrastructure",
22
+ "infrastructure-as-code",
23
+ "code-generation",
24
+ "app-manifest",
25
+ "typescript",
26
+ "bun",
27
+ "expo",
28
+ "react-native",
29
+ "supabase",
30
+ "postgres",
31
+ "kubernetes",
32
+ "minikube",
33
+ "openapi"
34
+ ],
35
+ "dependencies": {
36
+ "@ankhorage/contracts": "^1.15.0"
37
+ },
38
+ "files": [
39
+ "dist",
40
+ "src",
41
+ "package.json",
42
+ "README.md",
43
+ "CHANGELOG.md",
44
+ "LICENSE"
45
+ ],
46
+ "exports": {
47
+ ".": {
48
+ "types": "./dist/index.d.ts",
49
+ "import": "./dist/index.js",
50
+ "default": "./dist/index.js"
51
+ },
52
+ "./metadata": {
53
+ "types": "./dist/metadata/index.d.ts",
54
+ "import": "./dist/metadata/index.js",
55
+ "default": "./dist/metadata/index.js"
56
+ },
57
+ "./package.json": "./package.json"
58
+ },
59
+ "publishConfig": {
60
+ "access": "public"
61
+ },
62
+ "scripts": {
63
+ "build": "rm -rf dist tsconfig.tsbuildinfo && bun x tsc -p tsconfig.json",
64
+ "changeset": "changeset",
65
+ "changeset:status": "changeset status --since=origin/main",
66
+ "docs": "paradox",
67
+ "knip": "ankhorage-knip",
68
+ "lint": "ankhorage-eslint . --max-warnings=0",
69
+ "lint:fix": "ankhorage-eslint . --fix --max-warnings=0",
70
+ "format": "ankhorage-prettier --write .",
71
+ "format:check": "ankhorage-prettier --check .",
72
+ "prepack": "bun run build",
73
+ "test": "bun test src",
74
+ "typecheck": "bun x tsc --noEmit -p tsconfig.json",
75
+ "version-packages": "changeset version"
76
+ },
77
+ "devDependencies": {
78
+ "@ankhorage/devtools": "^1.0.6",
79
+ "@ankhorage/paradox": "^0.1.10",
80
+ "@changesets/cli": "^2.31.0",
81
+ "@types/bun": "^1.3.13",
82
+ "@types/node": "^25.6.0",
83
+ "typescript": "^5.9.3"
84
+ },
85
+ "packageManager": "bun@1.3.13"
86
+ }
@@ -0,0 +1,73 @@
1
+ # Minikube Adapter
2
+
3
+ Generates local Kubernetes artifacts under `infra/minikube/*` from `InfraManifest`.
4
+
5
+ ## Structure
6
+
7
+ - `base/`: namespace, app runtime, kustomization, helper scripts.
8
+ - `auth/`: auth provider artifacts (implemented: `supabase`).
9
+ - `authz/`: authorization engine artifacts (implemented: `cerbos`; `native` warning-only/no resources; `opa` unsupported).
10
+
11
+ Cerbos policy generation uses app manifest intent (navigator routes/screens + authFlow) when provided by caller.
12
+
13
+ ## Runtime Scripts
14
+
15
+ - `scripts/up.sh`: starts minikube (if needed) and applies all generated manifests.
16
+ - `scripts/build-app-image.sh`: exports Expo web build from app source and builds Docker image.
17
+ - `scripts/port-forward.sh`: forwards `service/app-runtime` to local HTTP (default `127.0.0.1:18080`).
18
+ - `scripts/status.sh`: validates kube context/API reachability and shows namespace resources.
19
+ - `scripts/down.sh`: deletes generated manifests from the cluster.
20
+ - `scripts/supabase-local-env.sh`: reads `supabase status -o env` and writes Supabase keys into `infra/minikube/.env`.
21
+
22
+ Build prerequisites for `build-app-image.sh`:
23
+
24
+ - `bun` / `bunx`
25
+ - `docker`
26
+
27
+ ## App Image Behavior
28
+
29
+ - Default app image: `ankh/<namespace>:dev` (for example `ankh/shop:dev`).
30
+ - By default `up.sh` triggers `build-app-image.sh` (`APP_BUILD_ENABLED=true`) before apply.
31
+ - `build-app-image.sh` runs `bunx expo export --platform web`, then builds via `app-image/Dockerfile`.
32
+ - `up.sh` syncs runtime image using `APP_IMAGE_SYNC_STRATEGY`:
33
+ - `docker-load` (default): loads existing local Docker image into Minikube.
34
+ - `minikube-build`: builds exported web artifacts directly into Minikube image store.
35
+ - `none`: skips local image sync.
36
+ - Project deletion removes the exact generated `APP_IMAGE` from Minikube and host Docker on a best-effort basis.
37
+ - Generated image cleanup defaults can be overridden in `infra/minikube/.env`:
38
+ - `APP_IMAGE_CLEANUP_ON_DOWN=false`: disables generated app image cleanup.
39
+ - `APP_IMAGE_CLEANUP_MINIKUBE=false`: keeps the Minikube image cache entry.
40
+ - `APP_IMAGE_CLEANUP_DOCKER=false`: keeps the host Docker image.
41
+ - Cleanup removes only the exact `APP_IMAGE`; it does not run global Docker image or network pruning.
42
+ - Private registries are supported via optional `.env` keys:
43
+ - `APP_BUILD_ENABLED`
44
+ - `APP_SOURCE_DIR`
45
+ - `SUPABASE_PROJECT_DIR`
46
+ - `APP_WEB_EXPORT_DIR`
47
+ - `APP_IMAGE_SYNC_STRATEGY`
48
+ - `AUTH_RUNTIME_MODE`
49
+ - `APP_PORT_FORWARD_LOCAL_PORT`
50
+ - `APP_PORT_FORWARD_REMOTE_PORT`
51
+ - `SUPABASE_SECRET_SYNC_ENABLED`
52
+ - `APP_IMAGE_PULL_SECRET_NAME`
53
+ - `APP_IMAGE_PULL_SECRET_SERVER`
54
+ - `APP_IMAGE_PULL_SECRET_USERNAME`
55
+ - `APP_IMAGE_PULL_SECRET_PASSWORD`
56
+ - `APP_IMAGE_PULL_SECRET_EMAIL`
57
+
58
+ When pull-secret values are provided, `up.sh` creates/updates the secret and patches
59
+ `deployment/app-runtime` with `imagePullSecrets`.
60
+
61
+ When `SUPABASE_SECRET_SYNC_ENABLED=true` and required Supabase keys are present,
62
+ `up.sh` creates/updates `secret/supabase-auth-secrets` from env values.
63
+
64
+ When `AUTH_RUNTIME_MODE=local` (default) and Supabase keys are missing, `up.sh`
65
+ auto-runs `scripts/supabase-local-env.sh`, reloads `.env`, and retries secret sync.
66
+ If `supabase/config.toml` is missing in `SUPABASE_PROJECT_DIR`, the helper now runs
67
+ `supabase init --yes` automatically before starting local Supabase.
68
+
69
+ ## Practical Selector Rule
70
+
71
+ When building config UI or pickers, only show currently supported values from adapter-owned support lists
72
+ (`DEPLOYMENT_TARGETS`, `DATABASE_PROVIDERS`, `AUTH_PROVIDERS`, `AUTH_SCOPES`, `AUTHZ_ENGINES`).
73
+ Do not show planned values until an adapter path exists in this folder.
@@ -0,0 +1,23 @@
1
+ import type { InfraManifestInput } from '../../../types';
2
+ import { emptyMinikubeArtifacts, type MinikubeAdapterArtifacts } from '../contracts';
3
+ import { generateSupabaseAuthArtifacts } from './supabase';
4
+
5
+ export function generateAuthProviderArtifacts(args: {
6
+ manifest: InfraManifestInput;
7
+ namespace: string;
8
+ }): MinikubeAdapterArtifacts {
9
+ const { manifest, namespace } = args;
10
+ const { auth } = manifest;
11
+
12
+ if (!auth) {
13
+ return emptyMinikubeArtifacts();
14
+ }
15
+
16
+ if (auth.provider !== 'supabase') {
17
+ throw new Error(
18
+ `Unsupported auth provider for minikube adapter: "${auth.provider}". Only "supabase" is currently supported.`,
19
+ );
20
+ }
21
+
22
+ return generateSupabaseAuthArtifacts({ manifest, namespace });
23
+ }
@@ -0,0 +1,269 @@
1
+ import type { InfraManifestInput } from '../../../../types';
2
+ import type { MinikubeAdapterArtifacts } from '../../contracts';
3
+
4
+ export function generateSupabaseAuthArtifacts(args: {
5
+ manifest: InfraManifestInput;
6
+ namespace: string;
7
+ }): MinikubeAdapterArtifacts {
8
+ const { manifest, namespace } = args;
9
+
10
+ const root = 'infra/minikube/k8s/auth/supabase';
11
+ const resourceRoot = 'auth/supabase';
12
+ const docsRoot = 'infra/minikube/auth';
13
+ const scope = manifest.auth?.scope ?? 'global';
14
+ const authzEngine = manifest.auth?.authorization.engine ?? 'native';
15
+ const authzKind = manifest.auth?.authorization.kind ?? 'RBAC';
16
+ const authFieldModel = resolveAuthFieldModel(manifest);
17
+
18
+ const warnings: string[] = [];
19
+ if (scope !== 'global') {
20
+ warnings.push(
21
+ `Auth scope "${scope}" is not fully modeled yet for Supabase. Baseline resources assume global auth.`,
22
+ );
23
+ }
24
+
25
+ return {
26
+ files: [
27
+ {
28
+ path: `${root}/supabase-auth.configmap.yaml`,
29
+ content: getSupabaseConfigMap({
30
+ namespace,
31
+ scope,
32
+ authzEngine,
33
+ authzKind,
34
+ authFieldModel,
35
+ }),
36
+ },
37
+ {
38
+ path: `${root}/supabase-auth.secret.yaml`,
39
+ content: getSupabaseSecret(namespace),
40
+ },
41
+ {
42
+ path: `${root}/app-runtime-auth.env.configmap.yaml`,
43
+ content: getRuntimeAuthEnvConfigMap({
44
+ namespace,
45
+ scope,
46
+ authzEngine,
47
+ authzKind,
48
+ authFieldModel,
49
+ }),
50
+ },
51
+ {
52
+ path: `${docsRoot}/supabase-runtime-wiring.md`,
53
+ content: getSupabaseRuntimeWiringGuide(),
54
+ },
55
+ ],
56
+ resources: [
57
+ `${resourceRoot}/supabase-auth.configmap.yaml`,
58
+ `${resourceRoot}/supabase-auth.secret.yaml`,
59
+ `${resourceRoot}/app-runtime-auth.env.configmap.yaml`,
60
+ ],
61
+ envEntries: [
62
+ 'SUPABASE_SECRET_SYNC_ENABLED=true',
63
+ 'SUPABASE_URL=',
64
+ 'SUPABASE_ANON_KEY=',
65
+ 'SUPABASE_SERVICE_ROLE_KEY=',
66
+ 'SUPABASE_JWT_SECRET=',
67
+ 'EXPO_PUBLIC_SUPABASE_URL=',
68
+ 'EXPO_PUBLIC_SUPABASE_ANON_KEY=',
69
+ ],
70
+ warnings,
71
+ };
72
+ }
73
+
74
+ function getSupabaseConfigMap(args: {
75
+ namespace: string;
76
+ scope: string;
77
+ authzEngine: string;
78
+ authzKind: string;
79
+ authFieldModel: ResolvedAuthFieldModel;
80
+ }) {
81
+ const { namespace, scope, authzEngine, authzKind, authFieldModel } = args;
82
+
83
+ return `apiVersion: v1
84
+ kind: ConfigMap
85
+ metadata:
86
+ name: supabase-auth-config
87
+ namespace: ${namespace}
88
+ data:
89
+ AUTH_PROVIDER: "supabase"
90
+ AUTH_SCOPE: "${scope}"
91
+ AUTHZ_ENGINE: "${authzEngine}"
92
+ AUTHZ_KIND: "${authzKind}"
93
+ AUTH_SIGN_IN_IDENTIFIERS: "${escapeYamlDoubleQuoted(authFieldModel.signInIdentifiersCsv)}"
94
+ AUTH_SIGN_UP_REQUIRED_FIELDS: "${escapeYamlDoubleQuoted(authFieldModel.signUpRequiredFieldsCsv)}"
95
+ AUTH_SIGN_UP_OPTIONAL_FIELDS: "${escapeYamlDoubleQuoted(authFieldModel.signUpOptionalFieldsCsv)}"
96
+ AUTH_SIGN_UP_POLICY: "${authFieldModel.signUpPolicy}"
97
+ AUTH_PROFILE_FIELDS: "${escapeYamlDoubleQuoted(authFieldModel.profileFieldsCsv)}"
98
+ `;
99
+ }
100
+
101
+ function getSupabaseSecret(namespace: string) {
102
+ return `apiVersion: v1
103
+ kind: Secret
104
+ metadata:
105
+ name: supabase-auth-secrets
106
+ namespace: ${namespace}
107
+ type: Opaque
108
+ stringData:
109
+ SUPABASE_URL: ""
110
+ SUPABASE_ANON_KEY: ""
111
+ SUPABASE_SERVICE_ROLE_KEY: ""
112
+ SUPABASE_JWT_SECRET: ""
113
+ EXPO_PUBLIC_SUPABASE_URL: ""
114
+ EXPO_PUBLIC_SUPABASE_ANON_KEY: ""
115
+ `;
116
+ }
117
+
118
+ function getRuntimeAuthEnvConfigMap(args: {
119
+ namespace: string;
120
+ scope: string;
121
+ authzEngine: string;
122
+ authzKind: string;
123
+ authFieldModel: ResolvedAuthFieldModel;
124
+ }) {
125
+ const { namespace, scope, authzEngine, authzKind, authFieldModel } = args;
126
+
127
+ return `apiVersion: v1
128
+ kind: ConfigMap
129
+ metadata:
130
+ name: app-runtime-auth-env
131
+ namespace: ${namespace}
132
+ data:
133
+ AUTH_REQUIRED: "true"
134
+ AUTH_PROVIDER: "supabase"
135
+ AUTH_SCOPE: "${scope}"
136
+ AUTHZ_ENGINE: "${authzEngine}"
137
+ AUTHZ_KIND: "${authzKind}"
138
+ AUTH_SIGN_IN_IDENTIFIERS: "${escapeYamlDoubleQuoted(authFieldModel.signInIdentifiersCsv)}"
139
+ AUTH_SIGN_UP_REQUIRED_FIELDS: "${escapeYamlDoubleQuoted(authFieldModel.signUpRequiredFieldsCsv)}"
140
+ AUTH_SIGN_UP_OPTIONAL_FIELDS: "${escapeYamlDoubleQuoted(authFieldModel.signUpOptionalFieldsCsv)}"
141
+ AUTH_SIGN_UP_POLICY: "${authFieldModel.signUpPolicy}"
142
+ AUTH_PROFILE_FIELDS: "${escapeYamlDoubleQuoted(authFieldModel.profileFieldsCsv)}"
143
+ AUTH_TOKEN_HEADER: "Authorization"
144
+ AUTH_TOKEN_PREFIX: "Bearer"
145
+ `;
146
+ }
147
+
148
+ function getSupabaseRuntimeWiringGuide() {
149
+ return `# Supabase Runtime Wiring
150
+
151
+ This file documents the runtime conventions generated by the minikube Supabase adapter.
152
+
153
+ ## Generated Kubernetes Objects
154
+
155
+ - \`ConfigMap/supabase-auth-config\`: high-level auth metadata.
156
+ - \`Secret/supabase-auth-secrets\`: Supabase keys and public runtime keys.
157
+ - \`ConfigMap/app-runtime-auth-env\`: auth runtime feature flags and authz metadata.
158
+
159
+ ## App Runtime Convention
160
+
161
+ Your app runtime deployment should import both:
162
+
163
+ - \`app-runtime-auth-env\` (configMap)
164
+ - \`supabase-auth-secrets\` (secret)
165
+
166
+ Example deployment fragment:
167
+
168
+ \`\`\`yaml
169
+ envFrom:
170
+ - configMapRef:
171
+ name: app-runtime-auth-env
172
+ - secretRef:
173
+ name: supabase-auth-secrets
174
+ \`\`\`
175
+
176
+ The generated base deployment at \`k8s/app/deployment.yaml\` already includes this wiring
177
+ with \`optional: true\` for auth sources.
178
+
179
+ ## Expected Environment Variables
180
+
181
+ - \`AUTH_REQUIRED\`
182
+ - \`AUTH_PROVIDER\`
183
+ - \`AUTH_SCOPE\`
184
+ - \`AUTHZ_ENGINE\`
185
+ - \`AUTHZ_KIND\`
186
+ - \`AUTH_SIGN_IN_IDENTIFIERS\`
187
+ - \`AUTH_SIGN_UP_REQUIRED_FIELDS\`
188
+ - \`AUTH_SIGN_UP_OPTIONAL_FIELDS\`
189
+ - \`AUTH_SIGN_UP_POLICY\`
190
+ - \`AUTH_PROFILE_FIELDS\`
191
+ - \`SUPABASE_SECRET_SYNC_ENABLED\`
192
+ - \`SUPABASE_URL\`
193
+ - \`SUPABASE_ANON_KEY\`
194
+ - \`SUPABASE_SERVICE_ROLE_KEY\`
195
+ - \`SUPABASE_JWT_SECRET\`
196
+ - \`EXPO_PUBLIC_SUPABASE_URL\`
197
+ - \`EXPO_PUBLIC_SUPABASE_ANON_KEY\`
198
+
199
+ ## Secret Sync
200
+
201
+ When \`SUPABASE_SECRET_SYNC_ENABLED=true\`, \`scripts/up.sh\` will create/update
202
+ \`Secret/supabase-auth-secrets\` from the loaded environment values.
203
+ `;
204
+ }
205
+
206
+ const DEFAULT_SIGN_IN_IDENTIFIERS = ['email'];
207
+ const DEFAULT_SIGN_UP_REQUIRED_FIELDS = ['email', 'password'];
208
+ const DEFAULT_SIGN_UP_OPTIONAL_FIELDS = ['firstName', 'lastName'];
209
+ const DEFAULT_SIGN_UP_POLICY = 'autoSignIn';
210
+ const DEFAULT_PROFILE_FIELDS = ['email', 'firstName', 'lastName'];
211
+
212
+ interface ResolvedAuthFieldModel {
213
+ signInIdentifiersCsv: string;
214
+ signUpRequiredFieldsCsv: string;
215
+ signUpOptionalFieldsCsv: string;
216
+ signUpPolicy: string;
217
+ profileFieldsCsv: string;
218
+ }
219
+
220
+ function resolveAuthFieldModel(manifest: InfraManifestInput): ResolvedAuthFieldModel {
221
+ const signInIdentifiers = normalizeFieldList(
222
+ manifest.auth?.signIn?.identifiers,
223
+ DEFAULT_SIGN_IN_IDENTIFIERS,
224
+ );
225
+ const signUpRequiredFields = normalizeFieldList(
226
+ manifest.auth?.signUp?.requiredFields,
227
+ DEFAULT_SIGN_UP_REQUIRED_FIELDS,
228
+ );
229
+ const signUpOptionalFields = normalizeFieldList(
230
+ manifest.auth?.signUp?.optionalFields,
231
+ DEFAULT_SIGN_UP_OPTIONAL_FIELDS,
232
+ ).filter((field) => !signUpRequiredFields.includes(field));
233
+ const signUpPolicy = manifest.auth?.signUp?.signUpPolicy ?? DEFAULT_SIGN_UP_POLICY;
234
+ const profileFields = normalizeFieldList(manifest.auth?.profile?.fields, DEFAULT_PROFILE_FIELDS);
235
+
236
+ return {
237
+ signInIdentifiersCsv: signInIdentifiers.join(','),
238
+ signUpRequiredFieldsCsv: signUpRequiredFields.join(','),
239
+ signUpOptionalFieldsCsv: signUpOptionalFields.join(','),
240
+ signUpPolicy,
241
+ profileFieldsCsv: profileFields.join(','),
242
+ };
243
+ }
244
+
245
+ function normalizeFieldList(
246
+ values: readonly string[] | undefined,
247
+ fallback: readonly string[],
248
+ ): string[] {
249
+ const source = values ?? fallback;
250
+ const next: string[] = [];
251
+
252
+ for (const value of source) {
253
+ const normalized = value.trim();
254
+ if (!normalized || next.includes(normalized)) {
255
+ continue;
256
+ }
257
+ next.push(normalized);
258
+ }
259
+
260
+ if (next.length > 0) {
261
+ return next;
262
+ }
263
+
264
+ return [...fallback];
265
+ }
266
+
267
+ function escapeYamlDoubleQuoted(value: string): string {
268
+ return value.replaceAll('\\', '\\\\').replaceAll('"', '\\"');
269
+ }