@rehearsal-db/core 0.1.0-beta.11 → 0.1.0-beta.12
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/CHANGELOG.md +37 -1
- package/README.md +9 -0
- package/docs/commands.md +3 -2
- package/docs/configuration.md +78 -12
- package/docs/getting-started.md +10 -1
- package/docs/runtime-policies.md +4 -2
- package/docs/troubleshooting.md +14 -3
- package/package.json +3 -2
- package/src/cli/init_command.mjs +17 -0
- package/src/cli/renderers.mjs +24 -0
- package/src/identity/provider_configuration.mjs +177 -0
- package/src/project/configuration.d.mts +11 -0
- package/src/project/configuration.mjs +155 -19
- package/src/runtime/plan.mjs +33 -0
- package/src/targets/supabase.mjs +11 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,41 @@ a Changelog, and versions will follow Semantic Versioning after the package exis
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## [0.1.0-beta.12] - 2026-10-04
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Supabase projects can declaratively configure local Google or GitHub OAuth in
|
|
13
|
+
`rehearsal.config.mjs`. Rehearsal derives the credential allowlist, injects only the
|
|
14
|
+
ignored owner-only environment values, and generates the exact loopback callback in
|
|
15
|
+
the disposable runtime without tracking secrets. Local signup requires an explicit
|
|
16
|
+
acknowledgment and is enabled only in that generated runtime copy.
|
|
17
|
+
- Branded, responsive documentation is now hosted on GitHub Pages with searchable
|
|
18
|
+
navigation, light and dark themes, and automatic deployment from `main`.
|
|
19
|
+
- The documentation home page and packaged README show the live monthly npm download
|
|
20
|
+
count and link directly to the npm package.
|
|
21
|
+
- Packed-package onboarding acceptance now proves both fresh root-config creation and a
|
|
22
|
+
real beta.11 upgrade that preserves the user's existing configuration byte for byte.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- The packaged README now links prominently to the hosted documentation, npm package,
|
|
27
|
+
and issue tracker. Package homepage metadata also points to the hosted documentation.
|
|
28
|
+
- GitHub Pages publishing uses the current Node.js 24 action generations without legacy
|
|
29
|
+
runtime warnings.
|
|
30
|
+
- `rehearsal init` now calls out important optional settings that are available but not
|
|
31
|
+
enabled before showing the current installed-release template.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- Existing Google or GitHub provider sections can transition to declarative ownership
|
|
36
|
+
without editing the tracked Supabase template. Rehearsal replaces only a compatible
|
|
37
|
+
section in the disposable runtime copy, while `doctor` refuses ambiguous or
|
|
38
|
+
safety-weakening provider settings before reset.
|
|
39
|
+
- Multi-target plain-text plans now print each declarative OAuth callback, and `init`
|
|
40
|
+
advertises the declarative replacement even when a project still uses legacy provider
|
|
41
|
+
fields.
|
|
42
|
+
|
|
8
43
|
## [0.1.0-beta.11] - 2026-10-03
|
|
9
44
|
|
|
10
45
|
### Added
|
|
@@ -314,7 +349,8 @@ a Changelog, and versions will follow Semantic Versioning after the package exis
|
|
|
314
349
|
publication uses short-lived trusted OIDC, and every release tag must already exist on
|
|
315
350
|
protected `main`.
|
|
316
351
|
|
|
317
|
-
[Unreleased]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.
|
|
352
|
+
[Unreleased]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.12...HEAD
|
|
353
|
+
[0.1.0-beta.12]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.11...v0.1.0-beta.12
|
|
318
354
|
[0.1.0-beta.11]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.10...v0.1.0-beta.11
|
|
319
355
|
[0.1.0-beta.10]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.9...v0.1.0-beta.10
|
|
320
356
|
[0.1.0-beta.9]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.8...v0.1.0-beta.9
|
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Rehearsal
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@rehearsal-db/core)
|
|
4
|
+
|
|
5
|
+
[Read the documentation](https://ddupasquier.github.io/rehearsal-db/) · [View on npm](https://www.npmjs.com/package/@rehearsal-db/core) · [Report a problem](https://github.com/Ddupasquier/rehearsal-db/issues)
|
|
6
|
+
|
|
3
7
|
Rehearsal tests PostgreSQL and Supabase migrations on your computer before you run them
|
|
4
8
|
anywhere important. It restores safe test data into a disposable local database, applies
|
|
5
9
|
only the migrations you approve, and runs your project's own test command.
|
|
@@ -55,6 +59,11 @@ docker pull postgres:17-alpine
|
|
|
55
59
|
|
|
56
60
|
Rehearsal itself never downloads a database image during a rehearsal.
|
|
57
61
|
|
|
62
|
+
For local Google or GitHub sign-in, the generated config includes a commented
|
|
63
|
+
`supabase.authentication` example. It names an ignored owner-only credential file;
|
|
64
|
+
Rehearsal generates the provider block and local callback inside the disposable runtime.
|
|
65
|
+
See [Supabase authentication](docs/configuration.md#supabase-authentication).
|
|
66
|
+
|
|
58
67
|
## Three terms you will see
|
|
59
68
|
|
|
60
69
|
- **Baseline:** a locked, safe starting copy of your schema and test data.
|
package/docs/commands.md
CHANGED
|
@@ -34,8 +34,9 @@ Storage files. `baseline create` and `baseline prepare` are local-only.
|
|
|
34
34
|
|
|
35
35
|
`setup --write` is idempotent: supported existing config files and completed runtime
|
|
36
36
|
files remain unchanged, while missing safe ignore entries may be appended. On upgrades,
|
|
37
|
-
`init`
|
|
38
|
-
|
|
37
|
+
`init` first lists important optional settings that are available but not enabled, then
|
|
38
|
+
prints the installed release's template for comparison. `init --write` never replaces an
|
|
39
|
+
existing config.
|
|
39
40
|
|
|
40
41
|
## Optional source preparation
|
|
41
42
|
|
package/docs/configuration.md
CHANGED
|
@@ -42,9 +42,21 @@ export default defineRehearsalConfig({
|
|
|
42
42
|
rehearsalConfig: "infrastructure/rehearsal/supabase/config.toml",
|
|
43
43
|
runtimeWorkdir: ".rehearsal/runtime",
|
|
44
44
|
|
|
45
|
-
// Optional
|
|
46
|
-
//
|
|
47
|
-
//
|
|
45
|
+
// Optional local OAuth connection. Secrets stay in the ignored owner-only file.
|
|
46
|
+
// Register http://127.0.0.1:58321/auth/v1/callback with the provider.
|
|
47
|
+
// authentication: {
|
|
48
|
+
// enableLocalSignup: true,
|
|
49
|
+
// environmentFile: ".env.rehearsal-service.local",
|
|
50
|
+
// providers: [
|
|
51
|
+
// {
|
|
52
|
+
// name: "google",
|
|
53
|
+
// clientIdEnvironmentVariable: "REHEARSAL_GOOGLE_CLIENT_ID",
|
|
54
|
+
// clientSecretEnvironmentVariable: "REHEARSAL_GOOGLE_CLIENT_SECRET",
|
|
55
|
+
// skipNonceCheck: false,
|
|
56
|
+
// emailOptional: false,
|
|
57
|
+
// },
|
|
58
|
+
// ],
|
|
59
|
+
// },
|
|
48
60
|
},
|
|
49
61
|
baseline: {
|
|
50
62
|
artifactDirectory: ".rehearsal",
|
|
@@ -124,7 +136,8 @@ safe, run setup again, and compare the two files before deleting either one.
|
|
|
124
136
|
After upgrading, run `npx rehearsal init` to print the current release's generated
|
|
125
137
|
template beside the path of your existing config. This comparison is read-only; adding a
|
|
126
138
|
new optional key remains your decision. `npx rehearsal init --write` still refuses to
|
|
127
|
-
replace an existing config.
|
|
139
|
+
replace an existing config. Rehearsal lists important optional settings that are
|
|
140
|
+
available but not enabled before printing the template, so they are easier to find.
|
|
128
141
|
|
|
129
142
|
### Workspaces and custom paths
|
|
130
143
|
|
|
@@ -247,19 +260,72 @@ negative controls, such as unauthorized, malformed, withheld, or missing records
|
|
|
247
260
|
response that merely avoids a server error does not prove that eligible data was
|
|
248
261
|
published or can be found.
|
|
249
262
|
|
|
250
|
-
## Supabase
|
|
263
|
+
## Supabase authentication
|
|
251
264
|
|
|
252
|
-
|
|
253
|
-
|
|
265
|
+
Rehearsal can configure a Google or GitHub OAuth connection in its disposable local
|
|
266
|
+
Supabase runtime. Declare the provider and the names of its credential variables:
|
|
254
267
|
|
|
255
268
|
```ts
|
|
256
|
-
|
|
257
|
-
|
|
269
|
+
authentication: {
|
|
270
|
+
enableLocalSignup: true,
|
|
271
|
+
environmentFile: ".env.rehearsal-service.local",
|
|
272
|
+
providers: [
|
|
273
|
+
{
|
|
274
|
+
name: "google",
|
|
275
|
+
clientIdEnvironmentVariable: "REHEARSAL_GOOGLE_CLIENT_ID",
|
|
276
|
+
clientSecretEnvironmentVariable: "REHEARSAL_GOOGLE_CLIENT_SECRET",
|
|
277
|
+
skipNonceCheck: false,
|
|
278
|
+
emailOptional: false,
|
|
279
|
+
},
|
|
280
|
+
],
|
|
281
|
+
},
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Create the ignored file, add the values, then restrict it to your account:
|
|
285
|
+
|
|
286
|
+
```dotenv
|
|
287
|
+
REHEARSAL_GOOGLE_CLIENT_ID=your-local-oauth-client-id
|
|
288
|
+
REHEARSAL_GOOGLE_CLIENT_SECRET=your-local-oauth-client-secret
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
chmod 600 .env.rehearsal-service.local
|
|
258
293
|
```
|
|
259
294
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
295
|
+
Register `http://127.0.0.1:<apiPort>/auth/v1/callback` with the provider, using the
|
|
296
|
+
`runtime.apiPort` value from this config. `rehearsal explain` also prints the exact URL.
|
|
297
|
+
Rehearsal writes that callback and `env(...)` references into the disposable runtime; it
|
|
298
|
+
never copies credential values into tracked configuration. Use `name: "github"` and
|
|
299
|
+
distinct GitHub variable names for GitHub. Duplicate providers, reused variable names,
|
|
300
|
+
unsupported providers, extra keys, and a template that already owns the same provider
|
|
301
|
+
are refused. This follows Supabase's documented
|
|
302
|
+
[`env()` configuration pattern](https://supabase.com/docs/guides/local-development/managing-config).
|
|
303
|
+
|
|
304
|
+
`enableLocalSignup: true` explicitly acknowledges that disposable local Auth may create
|
|
305
|
+
the temporary user returned by OAuth. Rehearsal changes the generated runtime copy only;
|
|
306
|
+
the tracked Supabase template and every hosted project remain unchanged.
|
|
307
|
+
|
|
308
|
+
The older `serviceEnvironmentFile`, `serviceEnvironmentVariables`, and
|
|
309
|
+
`safety.authenticationProviders` fields remain compatible for projects that manually
|
|
310
|
+
own their Supabase provider TOML. Do not combine those fields with the declarative
|
|
311
|
+
`authentication` block.
|
|
312
|
+
|
|
313
|
+
To migrate a manual Google or GitHub setup, replace those three legacy config fields
|
|
314
|
+
with the `authentication` block together. The tracked Supabase template may keep its
|
|
315
|
+
existing provider section: Rehearsal replaces a compatible legacy section only in the
|
|
316
|
+
generated disposable runtime copy. The section may contain `enabled`, `client_id`,
|
|
317
|
+
`secret`, `redirect_uri`, `skip_nonce_check`, and `email_optional`. The latter two must
|
|
318
|
+
match the explicit `skipNonceCheck` and `emailOptional` declarations, which default to
|
|
319
|
+
`false`. Keep them false unless the provider integration has a reviewed need; Supabase
|
|
320
|
+
warns in its [CLI configuration reference](https://supabase.com/docs/guides/local-development/cli/config#auth.external.provider.skip_nonce_check)
|
|
321
|
+
that skipping nonce validation reduces replay protection. Unknown settings,
|
|
322
|
+
duplicate sections, nested provider tables, or mismatched options fail
|
|
323
|
+
`rehearsal doctor` before reset. Rehearsal never edits the tracked template during this
|
|
324
|
+
transition.
|
|
325
|
+
|
|
326
|
+
Provider connection and copied-account association are separate. This block lets the
|
|
327
|
+
ordinary application sign-in reach local Auth. An optional `identityPolicy` decides
|
|
328
|
+
whether that verified local identity may claim a reviewed copied account.
|
|
263
329
|
|
|
264
330
|
## Safety fields
|
|
265
331
|
|
package/docs/getting-started.md
CHANGED
|
@@ -55,6 +55,13 @@ Supabase or PostgreSQL, and approve the same safe setup preview.
|
|
|
55
55
|
The proof command should check behavior changed by the migration. A test that only checks
|
|
56
56
|
whether the home page loads is usually too weak.
|
|
57
57
|
|
|
58
|
+
If the sandbox needs Google or GitHub sign-in, uncomment
|
|
59
|
+
`supabase.authentication` in `rehearsal.config.mjs`. Add the named credentials to
|
|
60
|
+
`.env.rehearsal-service.local`, run `chmod 600 .env.rehearsal-service.local`, and
|
|
61
|
+
register the local callback printed in the config. See
|
|
62
|
+
[Supabase authentication](configuration.md#supabase-authentication). Rehearsal keeps the
|
|
63
|
+
values out of tracked files and generates the disposable provider configuration for you.
|
|
64
|
+
|
|
58
65
|
## 3. Prepare safe baseline inputs
|
|
59
66
|
|
|
60
67
|
A **baseline** is the locked starting point restored before each rehearsal. Start with a
|
|
@@ -168,7 +175,9 @@ complete reference and [Troubleshooting](troubleshooting.md) when a check fails.
|
|
|
168
175
|
|
|
169
176
|
Running setup again is safe: existing configuration and runtime files are reported as
|
|
170
177
|
unchanged. Rehearsal only adds missing ignore entries. It never refreshes an existing
|
|
171
|
-
config during an upgrade.
|
|
178
|
+
config during an upgrade. After upgrading, run `npx rehearsal init` to see important
|
|
179
|
+
optional settings you have not enabled and compare your file with the installed
|
|
180
|
+
release's current template.
|
|
172
181
|
|
|
173
182
|
## Files Rehearsal creates
|
|
174
183
|
|
package/docs/runtime-policies.md
CHANGED
|
@@ -157,8 +157,10 @@ graph without an application callback patch:
|
|
|
157
157
|
|
|
158
158
|
Authentication has three separate parts:
|
|
159
159
|
|
|
160
|
-
1.
|
|
161
|
-
|
|
160
|
+
1. `config.supabase.authentication` can enable a Google or GitHub connection in local
|
|
161
|
+
Supabase and reads its credentials from the ignored owner-only environment file.
|
|
162
|
+
Existing projects may still own that local provider TOML manually. Email sign-in does
|
|
163
|
+
not use an OAuth credential declaration.
|
|
162
164
|
2. The application completes the ordinary sign-in and receives a normal Supabase
|
|
163
165
|
session. Rehearsal does not intercept the callback.
|
|
164
166
|
3. The identity policy matches that verified local identity and transfers only the
|
package/docs/troubleshooting.md
CHANGED
|
@@ -146,9 +146,20 @@ changes.
|
|
|
146
146
|
|
|
147
147
|
## Local authentication fails
|
|
148
148
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
149
|
+
For Google or GitHub, confirm `supabase.authentication` is declared in
|
|
150
|
+
`rehearsal.config.mjs`, its ignored environment file exists with `chmod 600`, and the
|
|
151
|
+
provider has the printed local Auth callback registered. Rehearsal generates the
|
|
152
|
+
disposable Supabase provider block.
|
|
153
|
+
|
|
154
|
+
When migrating from the legacy service environment fields, replace those fields together
|
|
155
|
+
with `supabase.authentication` and rerun `rehearsal doctor`. A compatible Google or
|
|
156
|
+
GitHub provider section may remain in the tracked template: Rehearsal supersedes it only
|
|
157
|
+
inside the generated runtime copy. Doctor refuses unknown provider settings, duplicate
|
|
158
|
+
or nested provider sections, and nonce or optional-email settings that do not match the
|
|
159
|
+
new declaration. Keep both options false unless there is a reviewed provider-specific
|
|
160
|
+
need; skipping nonce validation reduces replay protection. Otherwise keep using the
|
|
161
|
+
documented legacy configuration.
|
|
162
|
+
Never paste provider secrets into tracked config or terminal output.
|
|
152
163
|
|
|
153
164
|
If `identity plan` refuses the matcher, read the message literally:
|
|
154
165
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rehearsal-db/core",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.12",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Safely rehearse PostgreSQL and Supabase migrations against sanitized, production-shaped data.",
|
|
6
6
|
"repository": {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"bugs": {
|
|
11
11
|
"url": "https://github.com/Ddupasquier/rehearsal-db/issues"
|
|
12
12
|
},
|
|
13
|
-
"homepage": "https://github.
|
|
13
|
+
"homepage": "https://ddupasquier.github.io/rehearsal-db/",
|
|
14
14
|
"publishConfig": {
|
|
15
15
|
"access": "public",
|
|
16
16
|
"tag": "beta",
|
|
@@ -62,6 +62,7 @@
|
|
|
62
62
|
"test:fixture": "node scripts/verification/fixtures/supabase.mjs",
|
|
63
63
|
"test:fixture:postgresql": "node scripts/verification/fixtures/postgresql.mjs",
|
|
64
64
|
"test:fixture:dependent": "node scripts/verification/fixtures/dependent.mjs",
|
|
65
|
+
"test:fixture:onboarding": "node scripts/verification/fixtures/onboarding.mjs",
|
|
65
66
|
"test:fixture:standalone": "node scripts/verification/fixtures/standalone.mjs",
|
|
66
67
|
"test:fixture:privacy": "node scripts/verification/fixtures/privacy.mjs",
|
|
67
68
|
"check": "npm run check:syntax && npm run test && npm run docs:build && npm run format:check && npm run package:audit",
|
package/src/cli/init_command.mjs
CHANGED
|
@@ -49,11 +49,27 @@ export const runRehearsalInit = async ({ projectRoot, flags }) => {
|
|
|
49
49
|
applicationUrl: loaded.config.runtime.applicationUrl,
|
|
50
50
|
databasePort: loaded.config.runtime.ports.database,
|
|
51
51
|
});
|
|
52
|
+
const usesLegacyAuthentication = Boolean(
|
|
53
|
+
loaded.config.supabase?.serviceEnvironmentFile ||
|
|
54
|
+
loaded.config.safety.authenticationProviders.length,
|
|
55
|
+
);
|
|
56
|
+
const availableOptions =
|
|
57
|
+
loaded.config.supabase && !loaded.config.supabase.authentication
|
|
58
|
+
? [
|
|
59
|
+
{
|
|
60
|
+
path: "supabase.authentication",
|
|
61
|
+
summary: usesLegacyAuthentication
|
|
62
|
+
? "A declarative Google/GitHub replacement is available; replace the legacy provider fields together after review."
|
|
63
|
+
: "Configure local Google or GitHub sign-in with credentials kept outside tracked files.",
|
|
64
|
+
},
|
|
65
|
+
]
|
|
66
|
+
: [];
|
|
52
67
|
return {
|
|
53
68
|
mode: "current-template",
|
|
54
69
|
destination: relative(projectRoot, existingPath),
|
|
55
70
|
detected,
|
|
56
71
|
source,
|
|
72
|
+
availableOptions,
|
|
57
73
|
nextAction:
|
|
58
74
|
"Your existing configuration was not changed. Compare it with this installed-release template to discover new optional keys.",
|
|
59
75
|
};
|
|
@@ -70,6 +86,7 @@ export const runRehearsalInit = async ({ projectRoot, flags }) => {
|
|
|
70
86
|
destination: relative(projectRoot, destination),
|
|
71
87
|
detected,
|
|
72
88
|
source,
|
|
89
|
+
availableOptions: [],
|
|
73
90
|
nextAction: flags.write
|
|
74
91
|
? "Review the generated safety settings and run rehearsal doctor."
|
|
75
92
|
: "Review this preview, then rerun rehearsal init --write to create it.",
|
package/src/cli/renderers.mjs
CHANGED
|
@@ -14,6 +14,16 @@ export const renderPlan = (plan, verbosity) => {
|
|
|
14
14
|
...plan.targets.flatMap((target) => [
|
|
15
15
|
`${target.name} — ${target.plan.config.project}`,
|
|
16
16
|
` ✓ ${target.plan.environment.kind}`,
|
|
17
|
+
...(target.plan.environment.authenticationProviders.length
|
|
18
|
+
? [
|
|
19
|
+
` ✓ External identity providers ${target.plan.environment.authenticationProviders.join(", ")}`,
|
|
20
|
+
...(target.plan.environment.authenticationCallback
|
|
21
|
+
? [
|
|
22
|
+
` → Register callback ${target.plan.environment.authenticationCallback}`,
|
|
23
|
+
]
|
|
24
|
+
: []),
|
|
25
|
+
]
|
|
26
|
+
: []),
|
|
17
27
|
` ✓ baseline ${target.plan.baseline.generationId}`,
|
|
18
28
|
` ✓ ${formatCount(target.plan.baseline.tableCount, "table")}; ${formatCount(target.plan.baseline.rowCount, "row")}`,
|
|
19
29
|
...(target.plan.migrations.candidates.length
|
|
@@ -69,6 +79,11 @@ export const renderPlan = (plan, verbosity) => {
|
|
|
69
79
|
...(plan.environment.authenticationProviders.length
|
|
70
80
|
? [
|
|
71
81
|
` ✓ External identity providers ${plan.environment.authenticationProviders.join(", ")}`,
|
|
82
|
+
...(plan.environment.authenticationCallback
|
|
83
|
+
? [
|
|
84
|
+
` → Register callback ${plan.environment.authenticationCallback}`,
|
|
85
|
+
]
|
|
86
|
+
: []),
|
|
72
87
|
]
|
|
73
88
|
: []),
|
|
74
89
|
"",
|
|
@@ -194,6 +209,15 @@ export const renderInit = (result) =>
|
|
|
194
209
|
`Detected package manager: ${result.detected.packageManager}`,
|
|
195
210
|
`Detected Supabase config: ${result.detected.hasSupabaseConfig ? "yes" : "no"}`,
|
|
196
211
|
`Detected migrations: ${result.detected.hasMigrations ? "yes" : "no"}`,
|
|
212
|
+
...(result.availableOptions?.length
|
|
213
|
+
? [
|
|
214
|
+
"",
|
|
215
|
+
"AVAILABLE BUT NOT ENABLED",
|
|
216
|
+
...result.availableOptions.map(
|
|
217
|
+
(option) => ` → ${option.path}: ${option.summary}`,
|
|
218
|
+
),
|
|
219
|
+
]
|
|
220
|
+
: []),
|
|
197
221
|
...(result.source ? ["", result.source] : []),
|
|
198
222
|
result.nextAction,
|
|
199
223
|
].join("\n");
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/** Render reviewed local OAuth declarations into the disposable Supabase runtime. */
|
|
2
|
+
|
|
3
|
+
const escapePattern = (value) =>
|
|
4
|
+
value.replaceAll(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
5
|
+
|
|
6
|
+
const LEGACY_PROVIDER_KEYS = new Set([
|
|
7
|
+
"client_id",
|
|
8
|
+
"email_optional",
|
|
9
|
+
"enabled",
|
|
10
|
+
"redirect_uri",
|
|
11
|
+
"secret",
|
|
12
|
+
"skip_nonce_check",
|
|
13
|
+
]);
|
|
14
|
+
const SAFE_BOOLEAN_PROVIDER_KEYS = new Set([
|
|
15
|
+
"email_optional",
|
|
16
|
+
"skip_nonce_check",
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
const readTomlSections = (source) => {
|
|
20
|
+
const lines = source.split("\n");
|
|
21
|
+
const sections = [];
|
|
22
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
23
|
+
const match = /^\s*\[([^\]]+)\]\s*(?:#.*)?$/u.exec(lines[index]);
|
|
24
|
+
if (!match) continue;
|
|
25
|
+
if (sections.length) sections.at(-1).end = index;
|
|
26
|
+
sections.push({ name: match[1], start: index, end: lines.length });
|
|
27
|
+
}
|
|
28
|
+
return { lines, sections };
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const validateLegacyProviderSection = ({ lines, section, provider }) => {
|
|
32
|
+
const seen = new Set();
|
|
33
|
+
for (const line of lines.slice(section.start + 1, section.end)) {
|
|
34
|
+
if (!line.trim() || /^\s*#/u.test(line)) continue;
|
|
35
|
+
const assignment = /^\s*([A-Za-z0-9_]+)\s*=/u.exec(line);
|
|
36
|
+
if (!assignment || !LEGACY_PROVIDER_KEYS.has(assignment[1])) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`The local Supabase template auth.external.${provider.name} section contains an unsupported setting. Rehearsal left the tracked template unchanged.`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
const key = assignment[1];
|
|
42
|
+
if (seen.has(key)) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`The local Supabase template auth.external.${provider.name} section contains duplicate ${key} settings.`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
seen.add(key);
|
|
48
|
+
if (
|
|
49
|
+
SAFE_BOOLEAN_PROVIDER_KEYS.has(key) &&
|
|
50
|
+
!new RegExp(
|
|
51
|
+
`^\\s*${escapePattern(key)}\\s*=\\s*${
|
|
52
|
+
key === "skip_nonce_check"
|
|
53
|
+
? (provider.skipNonceCheck ?? false)
|
|
54
|
+
: (provider.emailOptional ?? false)
|
|
55
|
+
}\\s*(?:#.*)?$`,
|
|
56
|
+
"u",
|
|
57
|
+
).test(line)
|
|
58
|
+
) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`The local Supabase template auth.external.${provider.name}.${key} setting must match its explicit declarative authentication option before Rehearsal can replace the legacy provider in its disposable runtime copy.`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const inspectSupabaseAuthenticationProviderTransition = ({
|
|
67
|
+
source,
|
|
68
|
+
authentication,
|
|
69
|
+
}) => {
|
|
70
|
+
if (!authentication) return Object.freeze({ replacedProviders: [] });
|
|
71
|
+
const { lines, sections } = readTomlSections(source);
|
|
72
|
+
const replacementSections = [];
|
|
73
|
+
for (const provider of authentication.providers) {
|
|
74
|
+
const sectionName = `auth.external.${provider.name}`;
|
|
75
|
+
const nested = sections.filter((section) =>
|
|
76
|
+
section.name.startsWith(`${sectionName}.`),
|
|
77
|
+
);
|
|
78
|
+
if (nested.length) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`The local Supabase template contains unsupported nested ${sectionName} configuration. Rehearsal left the tracked template unchanged.`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
const matching = sections.filter((section) => section.name === sectionName);
|
|
84
|
+
if (matching.length > 1) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`The local Supabase template declares ${sectionName} more than once.`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
if (matching.length === 1) {
|
|
90
|
+
validateLegacyProviderSection({
|
|
91
|
+
lines,
|
|
92
|
+
section: matching[0],
|
|
93
|
+
provider,
|
|
94
|
+
});
|
|
95
|
+
replacementSections.push({ ...matching[0], provider: provider.name });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return Object.freeze({
|
|
99
|
+
replacedProviders: Object.freeze(
|
|
100
|
+
replacementSections.map(({ provider }) => provider),
|
|
101
|
+
),
|
|
102
|
+
replacementSections: Object.freeze(replacementSections),
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const renderSupabaseAuthenticationProviders = ({
|
|
107
|
+
authentication,
|
|
108
|
+
apiPort,
|
|
109
|
+
}) => {
|
|
110
|
+
if (!authentication) return "";
|
|
111
|
+
return authentication.providers
|
|
112
|
+
.map(
|
|
113
|
+
(
|
|
114
|
+
provider,
|
|
115
|
+
) => `# Generated by Rehearsal from config.supabase.authentication.
|
|
116
|
+
[auth.external.${provider.name}]
|
|
117
|
+
enabled = true
|
|
118
|
+
client_id = "env(${provider.clientIdEnvironmentVariable})"
|
|
119
|
+
secret = "env(${provider.clientSecretEnvironmentVariable})"
|
|
120
|
+
redirect_uri = "http://127.0.0.1:${apiPort}/auth/v1/callback"
|
|
121
|
+
${provider.skipNonceCheck ? "skip_nonce_check = true\n" : ""}${provider.emailOptional ? "email_optional = true\n" : ""}
|
|
122
|
+
`,
|
|
123
|
+
)
|
|
124
|
+
.join("\n");
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const enableLocalSignup = (source) => {
|
|
128
|
+
const lines = source.split("\n");
|
|
129
|
+
let inAuth = false;
|
|
130
|
+
let authSections = 0;
|
|
131
|
+
let signupSettings = 0;
|
|
132
|
+
const updated = lines.map((line) => {
|
|
133
|
+
const section = /^\s*\[([^\]]+)\]\s*(?:#.*)?$/u.exec(line);
|
|
134
|
+
if (section) {
|
|
135
|
+
inAuth = section[1] === "auth";
|
|
136
|
+
if (inAuth) authSections += 1;
|
|
137
|
+
return line;
|
|
138
|
+
}
|
|
139
|
+
if (!inAuth || !/^\s*enable_signup\s*=/u.test(line)) return line;
|
|
140
|
+
signupSettings += 1;
|
|
141
|
+
const comment = line.match(/\s+#.*$/u)?.[0] ?? "";
|
|
142
|
+
return `enable_signup = true${comment}`;
|
|
143
|
+
});
|
|
144
|
+
if (authSections !== 1 || signupSettings !== 1) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
"The local Supabase template must declare exactly one [auth] section with one enable_signup setting before Rehearsal can configure OAuth.",
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
return updated.join("\n");
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export const applySupabaseAuthenticationProviders = ({
|
|
153
|
+
source,
|
|
154
|
+
authentication,
|
|
155
|
+
apiPort,
|
|
156
|
+
}) => {
|
|
157
|
+
if (!authentication) return source;
|
|
158
|
+
const transition = inspectSupabaseAuthenticationProviderTransition({
|
|
159
|
+
source,
|
|
160
|
+
authentication,
|
|
161
|
+
});
|
|
162
|
+
const { lines } = readTomlSections(source);
|
|
163
|
+
const replacedIndexes = new Set(
|
|
164
|
+
transition.replacementSections.flatMap((section) =>
|
|
165
|
+
Array.from(
|
|
166
|
+
{ length: section.end - section.start },
|
|
167
|
+
(_, offset) => section.start + offset,
|
|
168
|
+
),
|
|
169
|
+
),
|
|
170
|
+
);
|
|
171
|
+
const transitionedSource = lines
|
|
172
|
+
.filter((_, index) => !replacedIndexes.has(index))
|
|
173
|
+
.join("\n");
|
|
174
|
+
const runtimeSource = enableLocalSignup(transitionedSource);
|
|
175
|
+
const separator = runtimeSource.endsWith("\n") ? "\n" : "\n\n";
|
|
176
|
+
return `${runtimeSource}${separator}${renderSupabaseAuthenticationProviders({ authentication, apiPort })}`;
|
|
177
|
+
};
|
|
@@ -8,6 +8,17 @@ export interface RehearsalConfig {
|
|
|
8
8
|
migrationDirectory: string;
|
|
9
9
|
rehearsalConfig: string;
|
|
10
10
|
runtimeWorkdir: string;
|
|
11
|
+
authentication?: {
|
|
12
|
+
enableLocalSignup: true;
|
|
13
|
+
environmentFile: string;
|
|
14
|
+
providers: Array<{
|
|
15
|
+
name: "google" | "github";
|
|
16
|
+
clientIdEnvironmentVariable: string;
|
|
17
|
+
clientSecretEnvironmentVariable: string;
|
|
18
|
+
skipNonceCheck?: boolean;
|
|
19
|
+
emailOptional?: boolean;
|
|
20
|
+
}>;
|
|
21
|
+
};
|
|
11
22
|
serviceEnvironmentFile?: string;
|
|
12
23
|
serviceEnvironmentVariables?: string[];
|
|
13
24
|
};
|
|
@@ -61,6 +61,7 @@ const POSTGRES_IMAGE_PATTERN = /^postgres:\d+(?:\.\d+)?-alpine$/u;
|
|
|
61
61
|
const SAFE_COMMAND_PATTERN = /^[^\n\r\0]+$/u;
|
|
62
62
|
const ENVIRONMENT_KEY_PATTERN = /^[A-Z][A-Z0-9_]*$/u;
|
|
63
63
|
const ENVIRONMENT_REFERENCE_PATTERN = /^[a-z0-9][a-z0-9-]*:[A-Z][A-Z0-9_]*$/u;
|
|
64
|
+
const SUPPORTED_SUPABASE_OAUTH_PROVIDERS = new Set(["github", "google"]);
|
|
64
65
|
|
|
65
66
|
const isPlainObject = (value) =>
|
|
66
67
|
value !== null &&
|
|
@@ -251,6 +252,7 @@ const normalizeConfig = (input) => {
|
|
|
251
252
|
"migrationDirectory",
|
|
252
253
|
"rehearsalConfig",
|
|
253
254
|
"runtimeWorkdir",
|
|
255
|
+
"authentication",
|
|
254
256
|
"serviceEnvironmentFile",
|
|
255
257
|
"serviceEnvironmentVariables",
|
|
256
258
|
],
|
|
@@ -437,8 +439,123 @@ const normalizeConfig = (input) => {
|
|
|
437
439
|
if (!Array.isArray(commands)) {
|
|
438
440
|
throw new Error("config.verification.commands must be an array.");
|
|
439
441
|
}
|
|
440
|
-
const
|
|
441
|
-
supabase?.
|
|
442
|
+
const authentication =
|
|
443
|
+
supabase?.authentication === undefined
|
|
444
|
+
? null
|
|
445
|
+
: assertPlainObject(
|
|
446
|
+
supabase.authentication,
|
|
447
|
+
"config.supabase.authentication",
|
|
448
|
+
);
|
|
449
|
+
let normalizedAuthentication = null;
|
|
450
|
+
if (authentication) {
|
|
451
|
+
assertKnownKeys(
|
|
452
|
+
authentication,
|
|
453
|
+
["enableLocalSignup", "environmentFile", "providers"],
|
|
454
|
+
"config.supabase.authentication",
|
|
455
|
+
);
|
|
456
|
+
if (authentication.enableLocalSignup !== true) {
|
|
457
|
+
throw new Error(
|
|
458
|
+
"config.supabase.authentication.enableLocalSignup must be true to create the disposable local identity used by an OAuth sign-in.",
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
if (
|
|
462
|
+
supabase.serviceEnvironmentFile !== undefined ||
|
|
463
|
+
supabase.serviceEnvironmentVariables !== undefined
|
|
464
|
+
) {
|
|
465
|
+
throw new Error(
|
|
466
|
+
"config.supabase.authentication cannot be combined with the legacy serviceEnvironmentFile or serviceEnvironmentVariables fields.",
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
if (safety.authenticationProviders !== undefined) {
|
|
470
|
+
throw new Error(
|
|
471
|
+
"config.safety.authenticationProviders is derived from config.supabase.authentication and must be omitted.",
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
if (
|
|
475
|
+
!Array.isArray(authentication.providers) ||
|
|
476
|
+
authentication.providers.length === 0
|
|
477
|
+
) {
|
|
478
|
+
throw new Error(
|
|
479
|
+
"config.supabase.authentication.providers must be a non-empty array.",
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
const providers = authentication.providers.map((entry, index) => {
|
|
483
|
+
const path = `config.supabase.authentication.providers[${index}]`;
|
|
484
|
+
const value = assertPlainObject(entry, path);
|
|
485
|
+
assertKnownKeys(
|
|
486
|
+
value,
|
|
487
|
+
[
|
|
488
|
+
"name",
|
|
489
|
+
"clientIdEnvironmentVariable",
|
|
490
|
+
"clientSecretEnvironmentVariable",
|
|
491
|
+
"skipNonceCheck",
|
|
492
|
+
"emailOptional",
|
|
493
|
+
],
|
|
494
|
+
path,
|
|
495
|
+
);
|
|
496
|
+
const name = assertNonEmptyString(value.name, `${path}.name`);
|
|
497
|
+
if (!SUPPORTED_SUPABASE_OAUTH_PROVIDERS.has(name)) {
|
|
498
|
+
throw new Error(
|
|
499
|
+
`${path}.name must be one of: ${[...SUPPORTED_SUPABASE_OAUTH_PROVIDERS].join(", ")}.`,
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
const environmentVariable = (key) => {
|
|
503
|
+
const variable = assertNonEmptyString(value[key], `${path}.${key}`);
|
|
504
|
+
if (!ENVIRONMENT_KEY_PATTERN.test(variable)) {
|
|
505
|
+
throw new Error(
|
|
506
|
+
`${path}.${key} must be a safe environment variable name.`,
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
return variable;
|
|
510
|
+
};
|
|
511
|
+
const optionalBoolean = (key) => {
|
|
512
|
+
if (value[key] === undefined) return false;
|
|
513
|
+
if (typeof value[key] !== "boolean") {
|
|
514
|
+
throw new Error(`${path}.${key} must be a boolean.`);
|
|
515
|
+
}
|
|
516
|
+
return value[key];
|
|
517
|
+
};
|
|
518
|
+
return Object.freeze({
|
|
519
|
+
name,
|
|
520
|
+
clientIdEnvironmentVariable: environmentVariable(
|
|
521
|
+
"clientIdEnvironmentVariable",
|
|
522
|
+
),
|
|
523
|
+
clientSecretEnvironmentVariable: environmentVariable(
|
|
524
|
+
"clientSecretEnvironmentVariable",
|
|
525
|
+
),
|
|
526
|
+
skipNonceCheck: optionalBoolean("skipNonceCheck"),
|
|
527
|
+
emailOptional: optionalBoolean("emailOptional"),
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
if (new Set(providers.map(({ name }) => name)).size !== providers.length) {
|
|
531
|
+
throw new Error(
|
|
532
|
+
"config.supabase.authentication.providers must not contain duplicate providers.",
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
const credentialVariables = providers.flatMap((provider) => [
|
|
536
|
+
provider.clientIdEnvironmentVariable,
|
|
537
|
+
provider.clientSecretEnvironmentVariable,
|
|
538
|
+
]);
|
|
539
|
+
if (new Set(credentialVariables).size !== credentialVariables.length) {
|
|
540
|
+
throw new Error(
|
|
541
|
+
"config.supabase.authentication provider environment variables must be unique.",
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
normalizedAuthentication = Object.freeze({
|
|
545
|
+
enableLocalSignup: true,
|
|
546
|
+
environmentFile: assertRelativePath(
|
|
547
|
+
authentication.environmentFile,
|
|
548
|
+
"config.supabase.authentication.environmentFile",
|
|
549
|
+
),
|
|
550
|
+
providers: Object.freeze(providers),
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
const serviceEnvironmentVariables = normalizedAuthentication
|
|
554
|
+
? normalizedAuthentication.providers.flatMap((provider) => [
|
|
555
|
+
provider.clientIdEnvironmentVariable,
|
|
556
|
+
provider.clientSecretEnvironmentVariable,
|
|
557
|
+
])
|
|
558
|
+
: (supabase?.serviceEnvironmentVariables ?? []);
|
|
442
559
|
if (!Array.isArray(serviceEnvironmentVariables)) {
|
|
443
560
|
throw new Error(
|
|
444
561
|
"config.supabase.serviceEnvironmentVariables must be an array.",
|
|
@@ -460,19 +577,23 @@ const normalizeConfig = (input) => {
|
|
|
460
577
|
);
|
|
461
578
|
}
|
|
462
579
|
if (
|
|
463
|
-
Boolean(
|
|
464
|
-
|
|
580
|
+
Boolean(
|
|
581
|
+
normalizedAuthentication?.environmentFile ??
|
|
582
|
+
supabase?.serviceEnvironmentFile,
|
|
583
|
+
) !== Boolean(normalizedServiceEnvironmentVariables.length)
|
|
465
584
|
) {
|
|
466
585
|
throw new Error(
|
|
467
586
|
"config.supabase.serviceEnvironmentFile and serviceEnvironmentVariables must be configured together.",
|
|
468
587
|
);
|
|
469
588
|
}
|
|
470
|
-
const authenticationProviders =
|
|
471
|
-
?
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
589
|
+
const authenticationProviders = normalizedAuthentication
|
|
590
|
+
? normalizedAuthentication.providers.map(({ name }) => name)
|
|
591
|
+
: safety.authenticationProviders
|
|
592
|
+
? assertStringArray(
|
|
593
|
+
safety.authenticationProviders,
|
|
594
|
+
"config.safety.authenticationProviders",
|
|
595
|
+
)
|
|
596
|
+
: [];
|
|
476
597
|
if (runtimeTarget === "postgresql" && authenticationProviders.length > 0) {
|
|
477
598
|
throw new Error(
|
|
478
599
|
"config.safety.authenticationProviders is available only for the Supabase target.",
|
|
@@ -500,12 +621,15 @@ const normalizeConfig = (input) => {
|
|
|
500
621
|
supabase.runtimeWorkdir,
|
|
501
622
|
"config.supabase.runtimeWorkdir",
|
|
502
623
|
),
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
624
|
+
authentication: normalizedAuthentication,
|
|
625
|
+
serviceEnvironmentFile: normalizedAuthentication
|
|
626
|
+
? normalizedAuthentication.environmentFile
|
|
627
|
+
: supabase.serviceEnvironmentFile
|
|
628
|
+
? assertRelativePath(
|
|
629
|
+
supabase.serviceEnvironmentFile,
|
|
630
|
+
"config.supabase.serviceEnvironmentFile",
|
|
631
|
+
)
|
|
632
|
+
: null,
|
|
509
633
|
serviceEnvironmentVariables: Object.freeze(
|
|
510
634
|
normalizedServiceEnvironmentVariables,
|
|
511
635
|
),
|
|
@@ -1005,9 +1129,21 @@ export default defineRehearsalConfig({
|
|
|
1005
1129
|
rehearsalConfig: "infrastructure/rehearsal/supabase/config.toml",
|
|
1006
1130
|
runtimeWorkdir: ".rehearsal/runtime",
|
|
1007
1131
|
|
|
1008
|
-
// Optional local
|
|
1009
|
-
//
|
|
1010
|
-
//
|
|
1132
|
+
// Optional local OAuth connection. Secrets stay in the ignored owner-only file.
|
|
1133
|
+
// Register http://127.0.0.1:${ports.api}/auth/v1/callback with the provider.
|
|
1134
|
+
// authentication: {
|
|
1135
|
+
// enableLocalSignup: true,
|
|
1136
|
+
// environmentFile: ".env.rehearsal-service.local",
|
|
1137
|
+
// providers: [
|
|
1138
|
+
// {
|
|
1139
|
+
// name: "google",
|
|
1140
|
+
// clientIdEnvironmentVariable: "REHEARSAL_GOOGLE_CLIENT_ID",
|
|
1141
|
+
// clientSecretEnvironmentVariable: "REHEARSAL_GOOGLE_CLIENT_SECRET",
|
|
1142
|
+
// skipNonceCheck: false,
|
|
1143
|
+
// emailOptional: false,
|
|
1144
|
+
// },
|
|
1145
|
+
// ],
|
|
1146
|
+
// },
|
|
1011
1147
|
},
|
|
1012
1148
|
|
|
1013
1149
|
// Immutable local baseline files. Rehearsal never reads production on your behalf.
|
package/src/runtime/plan.mjs
CHANGED
|
@@ -11,6 +11,10 @@ import { join } from "node:path";
|
|
|
11
11
|
import { pathToFileURL } from "node:url";
|
|
12
12
|
import { performance } from "node:perf_hooks";
|
|
13
13
|
import { verifyActiveBaseline } from "../baseline/artifact.mjs";
|
|
14
|
+
import {
|
|
15
|
+
applySupabaseAuthenticationProviders,
|
|
16
|
+
inspectSupabaseAuthenticationProviderTransition,
|
|
17
|
+
} from "../identity/provider_configuration.mjs";
|
|
14
18
|
import {
|
|
15
19
|
inspectDetectedProject,
|
|
16
20
|
loadRehearsalConfig,
|
|
@@ -233,6 +237,9 @@ export const buildRehearsalPlan = async (options = {}) => {
|
|
|
233
237
|
hostedAccess: config.safety.hostedAccess,
|
|
234
238
|
outboundNetwork: config.safety.outboundNetwork,
|
|
235
239
|
authenticationProviders: config.safety.authenticationProviders,
|
|
240
|
+
authenticationCallback: config.supabase?.authentication
|
|
241
|
+
? `http://127.0.0.1:${config.runtime.ports.api}/auth/v1/callback`
|
|
242
|
+
: null,
|
|
236
243
|
barriers: [
|
|
237
244
|
"versioned configuration accepts loopback hosts only",
|
|
238
245
|
`runtime uses a dedicated local ${config.runtime.target === "supabase" ? "Supabase workdir" : "PostgreSQL container and volume"} and project id`,
|
|
@@ -459,6 +466,32 @@ export const runRehearsalDoctor = async (options = {}) => {
|
|
|
459
466
|
remediation:
|
|
460
467
|
"Correct the missing project path in the Rehearsal configuration.",
|
|
461
468
|
},
|
|
469
|
+
...(config.supabase?.authentication
|
|
470
|
+
? [
|
|
471
|
+
{
|
|
472
|
+
id: "authentication-runtime-config",
|
|
473
|
+
label: "Local authentication configuration",
|
|
474
|
+
run: async () => {
|
|
475
|
+
const source = await readFile(paths.rehearsalConfig, "utf8");
|
|
476
|
+
const transition =
|
|
477
|
+
inspectSupabaseAuthenticationProviderTransition({
|
|
478
|
+
source,
|
|
479
|
+
authentication: config.supabase.authentication,
|
|
480
|
+
});
|
|
481
|
+
applySupabaseAuthenticationProviders({
|
|
482
|
+
source,
|
|
483
|
+
authentication: config.supabase.authentication,
|
|
484
|
+
apiPort: config.runtime.ports.api,
|
|
485
|
+
});
|
|
486
|
+
return transition.replacedProviders.length
|
|
487
|
+
? `compatible legacy ${transition.replacedProviders.join(", ")} provider configuration will be replaced only in the disposable runtime copy`
|
|
488
|
+
: "declarative provider configuration can be generated without changing the tracked template";
|
|
489
|
+
},
|
|
490
|
+
remediation:
|
|
491
|
+
"Review the named provider section and make skipNonceCheck or emailOptional match explicitly in config.supabase.authentication; otherwise keep the documented manual provider configuration.",
|
|
492
|
+
},
|
|
493
|
+
]
|
|
494
|
+
: []),
|
|
462
495
|
{
|
|
463
496
|
id: "sanitization-policy",
|
|
464
497
|
label: "Reviewed sanitization policy",
|
package/src/targets/supabase.mjs
CHANGED
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
} from "../runtime/migration_history.mjs";
|
|
45
45
|
import { loadRehearsalConfig } from "../project/configuration.mjs";
|
|
46
46
|
import { readRehearsalServiceEnvironment } from "../runtime/service_environment.mjs";
|
|
47
|
+
import { applySupabaseAuthenticationProviders } from "../identity/provider_configuration.mjs";
|
|
47
48
|
import { readBoundRuntimeSanitizationPolicy } from "../baseline/sanitization_policy.mjs";
|
|
48
49
|
import {
|
|
49
50
|
captureLocalPublicSchema,
|
|
@@ -315,7 +316,16 @@ const prepareRuntimeWorkdir = async ({ baseline, paths }) => {
|
|
|
315
316
|
recursive: true,
|
|
316
317
|
mode: 0o700,
|
|
317
318
|
});
|
|
318
|
-
await
|
|
319
|
+
const configTemplate = await readFile(configTemplatePath, "utf8");
|
|
320
|
+
await writeFile(
|
|
321
|
+
join(runtimeSupabaseDirectory, "config.toml"),
|
|
322
|
+
applySupabaseAuthenticationProviders({
|
|
323
|
+
source: configTemplate,
|
|
324
|
+
authentication: config.supabase.authentication,
|
|
325
|
+
apiPort: config.runtime.ports.api,
|
|
326
|
+
}),
|
|
327
|
+
{ mode: 0o600 },
|
|
328
|
+
);
|
|
319
329
|
for (const filename of Object.keys(baseline.migrations).sort()) {
|
|
320
330
|
await cp(
|
|
321
331
|
join(paths.migrationsDirectory, filename),
|