@rehearsal-db/core 1.0.2 → 1.1.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.
- package/CHANGELOG.md +29 -3
- package/COMPATIBILITY.md +10 -4
- package/README.md +2 -2
- package/dist/package.json +1 -1
- package/dist/src/project/configuration_contract.d.mts +1 -0
- package/dist/src/project/configuration_normalization.mjs +2 -1
- package/dist/src/project/configuration_rendering.mjs +3 -2
- package/dist/src/project/configuration_resolution.mjs +0 -2
- package/dist/src/project/project_detection.d.mts +2 -0
- package/dist/src/project/project_detection.mjs +1 -0
- package/dist/src/project/runtime_project_identity.d.mts +5 -0
- package/dist/src/project/runtime_project_identity.mjs +41 -0
- package/dist/src/project/setup.mjs +3 -1
- package/dist/src/runtime/policy.d.mts +2 -0
- package/dist/src/runtime/policy.mjs +13 -1
- package/dist/src/runtime/privileges.d.mts +25 -0
- package/dist/src/runtime/privileges.mjs +198 -0
- package/dist/src/targets/supabase.mjs +1 -0
- package/docs/README.md +1 -1
- package/docs/configuration.md +19 -4
- package/docs/getting-started.md +4 -4
- package/docs/releasing.md +4 -2
- package/docs/roadmap.md +30 -9
- package/docs/runtime-lifecycle.md +5 -4
- package/docs/runtime-policies.md +145 -0
- package/docs/stability.md +8 -4
- package/docs/vscode.md +21 -16
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
All notable Rehearsal package changes
|
|
4
|
-
a Changelog, and versions
|
|
3
|
+
All notable Rehearsal package changes are documented here. The format follows Keep
|
|
4
|
+
a Changelog, and published versions follow Semantic Versioning.
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.1.0] - 2026-10-21
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Optional read-only runtime privilege expectations for reviewed schemas, tables,
|
|
13
|
+
sequences and exact function signatures. Required and forbidden effective access is
|
|
14
|
+
separate from RLS, includes inherited and column-only permissions, and never repairs
|
|
15
|
+
grants automatically. Existing policies without this field keep their behavior.
|
|
16
|
+
- Document explicit least-privilege Data API grants, forward-only correction and
|
|
17
|
+
positive/negative API proofs for Supabase's October 30 opt-in table-exposure change.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Record the Supabase provider in newly written runtime receipts, so inventory can
|
|
22
|
+
distinguish recorded format/provider compatibility from unknown legacy provenance.
|
|
23
|
+
Older receipts remain unknown rather than being silently rewritten or reverified.
|
|
24
|
+
- Bound newly generated Supabase project IDs to the provider's 40-character limit
|
|
25
|
+
with a deterministic suffix. Preserve valid existing identities; reject invalid
|
|
26
|
+
configured IDs without renaming or adopting resources.
|
|
27
|
+
- Align OS setup helpers with core's Node.js 22, 24 and 26 support. macOS/Linux
|
|
28
|
+
helpers accept an explicit `REHEARSAL_NODE_PATH`, and missing-Node guidance points
|
|
29
|
+
to version-manager selection or the official installer without installing an OS tool.
|
|
30
|
+
- Update compatible older CLI installations to the helper's exact current target
|
|
31
|
+
instead of treating every 1.0.1-or-newer installation as already current.
|
|
32
|
+
|
|
8
33
|
## [1.0.2] - 2026-10-20
|
|
9
34
|
|
|
10
35
|
### Fixed
|
|
@@ -815,7 +840,8 @@ not unattended cleanup, and container age is not proof of user inactivity.
|
|
|
815
840
|
publication uses short-lived trusted OIDC, and every release tag must already exist on
|
|
816
841
|
protected `main`.
|
|
817
842
|
|
|
818
|
-
[Unreleased]: https://github.com/Ddupasquier/rehearsal-db/compare/v1.0
|
|
843
|
+
[Unreleased]: https://github.com/Ddupasquier/rehearsal-db/compare/v1.1.0...HEAD
|
|
844
|
+
[1.1.0]: https://github.com/Ddupasquier/rehearsal-db/compare/v1.0.2...v1.1.0
|
|
819
845
|
[1.0.2]: https://github.com/Ddupasquier/rehearsal-db/compare/v1.0.1...v1.0.2
|
|
820
846
|
[1.0.1]: https://github.com/Ddupasquier/rehearsal-db/compare/v1.0.0...v1.0.1
|
|
821
847
|
[1.0.0]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-rc.10...v1.0.0
|
package/COMPATIBILITY.md
CHANGED
|
@@ -26,12 +26,18 @@ The first `1.0.0` release requires external beta evidence, a support policy and
|
|
|
26
26
|
settled public API described in [the stability policy](docs/stability.md). Supporting
|
|
27
27
|
additional database families, package managers or operating systems is not implied.
|
|
28
28
|
|
|
29
|
-
Rehearsal supports maintained
|
|
30
|
-
matrix
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
Rehearsal supports the maintained Node.js release lines verified by its existing CI
|
|
30
|
+
matrix: Node.js 22, 24, and 26. Node.js 24 LTS is the recommended default and the
|
|
31
|
+
release-build runtime. End-of-life and unverified future release lines are not
|
|
32
|
+
supported. A future release may remove a Node.js line after its upstream end-of-life
|
|
33
33
|
date, with that change called out in the changelog.
|
|
34
34
|
|
|
35
|
+
Core 1.1.0's OS helpers accept the same three lines. A terminal or desktop launcher
|
|
36
|
+
that cannot find Node does not mean an installed version is unsupported. Select it
|
|
37
|
+
through the project's existing version manager or use the helper's explicit executable
|
|
38
|
+
path; see [OS setup](installers/README.md). Helpers never install Node or switch a
|
|
39
|
+
project's runtime automatically.
|
|
40
|
+
|
|
35
41
|
## Operating systems and execution location
|
|
36
42
|
|
|
37
43
|
Core 1.0.1's platform boundary distinguishes the computer running VS Code from the
|
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ it through a reviewed privacy policy, and retire the access again. It is never p
|
|
|
47
47
|
production deployment tool.
|
|
48
48
|
|
|
49
49
|
The CLI is free and MIT-licensed, with stable releases on
|
|
50
|
-
[npm](https://www.npmjs.com/package/@rehearsal-db/core). This guide targets **1.0
|
|
50
|
+
[npm](https://www.npmjs.com/package/@rehearsal-db/core). This guide targets **1.1.0**;
|
|
51
51
|
install only a version actually published there. Prefer a visual workflow?
|
|
52
52
|
[Install the free VS Code Starter preview](docs/vscode.md). Its included features are
|
|
53
53
|
free indefinitely for personal or commercial development—no account, card, or trial.
|
|
@@ -71,7 +71,7 @@ your migration is intended to change.
|
|
|
71
71
|
|
|
72
72
|
Prefer OS-specific files? Use the [Windows / WSL 2](installers/windows/README.md),
|
|
73
73
|
[macOS](installers/macos/README.md) or [Linux](installers/linux/README.md) setup folder.
|
|
74
|
-
These readable helpers
|
|
74
|
+
These readable helpers accept existing Node.js 22, 24 or 26 and check prerequisites by
|
|
75
75
|
default; installation is an explicit option. They do not install OS tools or start a
|
|
76
76
|
database. See [their exact behavior and release availability](installers/README.md).
|
|
77
77
|
|
package/dist/package.json
CHANGED
|
@@ -126,6 +126,7 @@ export interface RehearsalConfig {
|
|
|
126
126
|
protected?: boolean;
|
|
127
127
|
target?: "supabase" | "postgresql";
|
|
128
128
|
applicationUrl?: string;
|
|
129
|
+
/** Exact lowercase/digit/hyphen ownership ID: 2–40 Supabase, 2–63 PostgreSQL. */
|
|
129
130
|
projectId?: string;
|
|
130
131
|
apiPort?: number;
|
|
131
132
|
databasePort: number;
|
|
@@ -9,6 +9,7 @@ import { normalizeResourcePolicy, normalizeRequiredServices, } from "./configura
|
|
|
9
9
|
import { normalizeConfigurationProfiles } from "./configuration_profiles.mjs";
|
|
10
10
|
import { normalizeApplicationUrls } from "./configuration_application_urls.mjs";
|
|
11
11
|
import { normalizeApplicationActivity } from "./configuration_activity.mjs";
|
|
12
|
+
import { assertRuntimeProjectId } from "./runtime_project_identity.mjs";
|
|
12
13
|
import { assertBoolean, assertCommand, assertDatabaseIdentifier, assertEnvironmentKey, assertEnvironmentReference, assertHttpStatus, assertIdentifier, assertKnownKeys, assertLoopbackUrl, assertNonEmptyString, assertPlainObject, assertPort, assertPostgresImage, assertPositiveInteger, assertRelativePath, assertStringArray, } from "./configuration_validation.mjs";
|
|
13
14
|
const LOOPBACK_HOSTS = new Set(REHEARSAL_DEFAULTS.safety.allowedHosts);
|
|
14
15
|
const SUPPORTED_SUPABASE_OAUTH_PROVIDERS = new Set(["github", "google"]);
|
|
@@ -351,7 +352,7 @@ export const normalizeRehearsalConfig = (input) => {
|
|
|
351
352
|
protected: assertBoolean(runtime.protected ?? false, "config.runtime.protected"),
|
|
352
353
|
target: runtimeTarget,
|
|
353
354
|
applicationUrl: applicationUrls.applicationUrl,
|
|
354
|
-
projectId:
|
|
355
|
+
projectId: assertRuntimeProjectId(runtime.projectId ?? REHEARSAL_DEFAULTS.runtime.projectId, runtimeTarget),
|
|
355
356
|
ports: Object.freeze(ports),
|
|
356
357
|
}),
|
|
357
358
|
safety: Object.freeze({
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** Render reviewed starter configurations without reading or writing a project. */
|
|
2
|
+
import { createRuntimeProjectId } from "./runtime_project_identity.mjs";
|
|
2
3
|
export const renderDetectedConfig = (detected, { applicationUrl = "http://localhost:5175", ports = { api: 58321, database: 58322, studio: 58323 }, } = {}) => `// @ts-check
|
|
3
4
|
/**
|
|
4
5
|
* Rehearsal configuration for ${detected.projectName}.
|
|
@@ -132,7 +133,7 @@ export default defineRehearsalConfig({
|
|
|
132
133
|
protected: false, // Pin data against reset, run, migrate, discard, and runtime cleanup.
|
|
133
134
|
target: "supabase",
|
|
134
135
|
applicationUrl: ${JSON.stringify(applicationUrl)},
|
|
135
|
-
projectId:
|
|
136
|
+
projectId: ${JSON.stringify(createRuntimeProjectId(detected.projectName, "supabase", detected.projectIdSeed))},
|
|
136
137
|
apiPort: ${ports.api},
|
|
137
138
|
databasePort: ${ports.database},
|
|
138
139
|
studioPort: ${ports.studio},
|
|
@@ -266,7 +267,7 @@ export default defineRehearsalConfig({
|
|
|
266
267
|
protected: false, // Pin data against reset, run, migrate, discard, and runtime cleanup.
|
|
267
268
|
target: "postgresql",
|
|
268
269
|
applicationUrl: ${JSON.stringify(applicationUrl)},
|
|
269
|
-
projectId:
|
|
270
|
+
projectId: ${JSON.stringify(createRuntimeProjectId(detected.projectName, "postgresql", detected.projectIdSeed))},
|
|
270
271
|
databasePort: ${databasePort},
|
|
271
272
|
},
|
|
272
273
|
|
|
@@ -3,7 +3,6 @@ import { basename, isAbsolute, join, relative, resolve } from "node:path";
|
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
import { findRehearsalConfigPath, } from "./configuration_discovery.mjs";
|
|
5
5
|
import { normalizeRehearsalConfig } from "./configuration_normalization.mjs";
|
|
6
|
-
import { assertIdentifier } from "./configuration_validation.mjs";
|
|
7
6
|
export const loadRehearsalConfig = async ({ projectRoot = process.cwd(), configPath, } = {}) => {
|
|
8
7
|
const root = resolve(projectRoot);
|
|
9
8
|
const path = await findRehearsalConfigPath({
|
|
@@ -74,7 +73,6 @@ export const loadRehearsalConfig = async ({ projectRoot = process.cwd(), configP
|
|
|
74
73
|
throw new Error("config.preparation.privacyKey must resolve to a secret file inside config.baseline.artifactDirectory.");
|
|
75
74
|
}
|
|
76
75
|
}
|
|
77
|
-
assertIdentifier(config.runtime.projectId, "config.runtime.projectId");
|
|
78
76
|
return Object.freeze({
|
|
79
77
|
config,
|
|
80
78
|
configPath: path,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/** Inspect project-local files used to render a conservative starter config. */
|
|
2
2
|
export interface DetectedProject {
|
|
3
3
|
projectName: string;
|
|
4
|
+
/** Untruncated normalized name keeps shortened new runtime identities distinct. */
|
|
5
|
+
projectIdSeed?: string;
|
|
4
6
|
packageManager: "npm" | "pnpm" | "yarn";
|
|
5
7
|
hasSupabaseConfig: boolean;
|
|
6
8
|
hasMigrations: boolean;
|
|
@@ -31,6 +31,7 @@ export const inspectDetectedProject = async ({ projectRoot = process.cwd(), } =
|
|
|
31
31
|
"rehearsal-project";
|
|
32
32
|
return {
|
|
33
33
|
projectName,
|
|
34
|
+
projectIdSeed: normalizedProjectName || projectName,
|
|
34
35
|
packageManager,
|
|
35
36
|
hasSupabaseConfig: await hasPath("supabase/config.toml"),
|
|
36
37
|
hasMigrations: await hasPath("supabase/migrations"),
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Preserve exact configured runtime ownership and bound newly generated identities. */
|
|
2
|
+
import type { RehearsalRuntimeTargetId } from "../targets/target.mjs";
|
|
3
|
+
export declare const assertRuntimeProjectId: (value: unknown, target: RehearsalRuntimeTargetId) => string;
|
|
4
|
+
/** Only new setup identities are shortened; valid existing generated IDs stay stable. */
|
|
5
|
+
export declare const createRuntimeProjectId: (projectName: string, target: RehearsalRuntimeTargetId, identitySeed?: string) => string;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** Preserve exact configured runtime ownership and bound newly generated identities. */
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { RehearsalError } from "../shared/diagnostics.mjs";
|
|
4
|
+
import { assertIdentifier, assertNonEmptyString, } from "./configuration_validation.mjs";
|
|
5
|
+
const maximumProjectIdLength = (target) => target === "supabase" ? 40 : 63;
|
|
6
|
+
export const assertRuntimeProjectId = (value, target) => {
|
|
7
|
+
const projectId = assertNonEmptyString(value, "config.runtime.projectId");
|
|
8
|
+
const maximumLength = maximumProjectIdLength(target);
|
|
9
|
+
if (projectId.length < 2 || projectId.length > maximumLength) {
|
|
10
|
+
const provider = target === "supabase" ? "Supabase" : "PostgreSQL";
|
|
11
|
+
throw new RehearsalError({
|
|
12
|
+
category: "configuration_invalid",
|
|
13
|
+
code: "CONFIGURATION_INVALID",
|
|
14
|
+
message: `config.runtime.projectId must contain 2–${maximumLength} characters for ${provider}.`,
|
|
15
|
+
expected: `2–${maximumLength} lowercase letters, digits, or hyphens`,
|
|
16
|
+
actual: { length: projectId.length },
|
|
17
|
+
context: { field: "runtime.projectId", target, maximumLength },
|
|
18
|
+
refused: "No runtime was started or renamed. Rehearsal never truncates a configured project ID or adopts resources by prefix.",
|
|
19
|
+
suggestions: [
|
|
20
|
+
"Choose a distinct, explicitly reviewed ID for a new disposable runtime. Review existing resources before changing their configured identity.",
|
|
21
|
+
],
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return assertIdentifier(projectId, "config.runtime.projectId");
|
|
25
|
+
};
|
|
26
|
+
/** Only new setup identities are shortened; valid existing generated IDs stay stable. */
|
|
27
|
+
export const createRuntimeProjectId = (projectName, target, identitySeed = projectName) => {
|
|
28
|
+
const candidate = `${projectName}-rehearsal`;
|
|
29
|
+
const maximumLength = maximumProjectIdLength(target);
|
|
30
|
+
if (candidate.length <= maximumLength)
|
|
31
|
+
return assertRuntimeProjectId(candidate, target);
|
|
32
|
+
const digest = createHash("sha256")
|
|
33
|
+
.update(`${identitySeed}-rehearsal`)
|
|
34
|
+
.digest("hex")
|
|
35
|
+
.slice(0, 12);
|
|
36
|
+
const suffix = `-${digest}-rehearsal`;
|
|
37
|
+
const prefix = projectName
|
|
38
|
+
.slice(0, maximumLength - suffix.length)
|
|
39
|
+
.replace(/-+$/u, "");
|
|
40
|
+
return assertRuntimeProjectId(`${prefix}${suffix}`, target);
|
|
41
|
+
};
|
|
@@ -8,6 +8,7 @@ import { access, mkdir, open, readFile, rename, rm, writeFile, } from "node:fs/p
|
|
|
8
8
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
9
9
|
import { findRehearsalConfigPath, inspectDetectedProject, loadRehearsalConfig, renderDetectedConfig, renderDetectedPostgresqlConfig, } from "./configuration.mjs";
|
|
10
10
|
import { resolveRuntimeTarget } from "../targets/target.mjs";
|
|
11
|
+
import { createRuntimeProjectId } from "./runtime_project_identity.mjs";
|
|
11
12
|
export { assertSupportedRehearsalNodeRuntime, inspectRehearsalNodeRuntime, } from "../shared/node_runtime.mjs";
|
|
12
13
|
import { assertSupportedRehearsalNodeRuntime } from "../shared/node_runtime.mjs";
|
|
13
14
|
const CONFIG_PATH = "rehearsal.config.mjs";
|
|
@@ -257,7 +258,8 @@ export const planRehearsalSetup = async ({ projectRoot = process.cwd(), configPa
|
|
|
257
258
|
...(isPortAvailable ? { isAvailable: isPortAvailable } : {}),
|
|
258
259
|
});
|
|
259
260
|
const applicationUrl = existing?.config.runtime.applicationUrl ?? "http://localhost:5175";
|
|
260
|
-
const projectId = existing?.config.runtime.projectId ??
|
|
261
|
+
const projectId = existing?.config.runtime.projectId ??
|
|
262
|
+
createRuntimeProjectId(detected.projectName, runtimeTarget, detected.projectIdSeed);
|
|
261
263
|
const sourceSupabaseConfig = runtimeTarget === "supabase"
|
|
262
264
|
? await readOptionalFile(join(root, "supabase/config.toml"))
|
|
263
265
|
: null;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** Build narrowly validated local restore prerequisites and structural checks. */
|
|
2
|
+
import { type RuntimePrivilege } from "./privileges.mjs";
|
|
2
3
|
type PolicyObject = Record<string, unknown>;
|
|
3
4
|
interface RuntimeRelation {
|
|
4
5
|
readonly schema: string;
|
|
@@ -55,6 +56,7 @@ export interface RuntimePolicy {
|
|
|
55
56
|
readonly triggers: readonly RuntimeTrigger[];
|
|
56
57
|
readonly localRows: readonly RuntimeLocalRow[];
|
|
57
58
|
readonly expectations: readonly RuntimeExpectation[];
|
|
59
|
+
readonly privileges?: readonly RuntimePrivilege[];
|
|
58
60
|
}
|
|
59
61
|
export declare const IDENTITY_CLAIM_RECEIPT_SQL = "create schema if not exists rehearsal_internal;\ncreate table if not exists rehearsal_internal.identity_claims (\n identity_name text primary key,\n placeholder_user_id uuid not null unique,\n local_user_id uuid not null,\n claimed_at timestamptz not null default now()\n);";
|
|
60
62
|
export declare const validateRuntimePolicy: (input: unknown) => RuntimePolicy;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** Build narrowly validated local restore prerequisites and structural checks. */
|
|
2
|
+
import { buildRuntimePrivilegeVerificationChecks, validateRuntimePrivileges, } from "./privileges.mjs";
|
|
2
3
|
const IDENTIFIER = /^[a-z][a-z0-9_]{0,62}$/u;
|
|
3
4
|
const IDENTITY_NAME = /^[a-z0-9][a-z0-9-]{1,62}$/u;
|
|
4
5
|
const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/iu;
|
|
@@ -58,7 +59,14 @@ create table if not exists rehearsal_internal.identity_claims (
|
|
|
58
59
|
);`;
|
|
59
60
|
export const validateRuntimePolicy = (input) => {
|
|
60
61
|
const policy = object(input, "runtimePolicy");
|
|
61
|
-
keys(policy, [
|
|
62
|
+
keys(policy, [
|
|
63
|
+
"policyVersion",
|
|
64
|
+
"prerequisites",
|
|
65
|
+
"triggers",
|
|
66
|
+
"localRows",
|
|
67
|
+
"expectations",
|
|
68
|
+
"privileges",
|
|
69
|
+
], "runtimePolicy");
|
|
62
70
|
if (policy.policyVersion !== 1)
|
|
63
71
|
throw new Error("Runtime policy must use policyVersion 1.");
|
|
64
72
|
const prerequisites = object(policy.prerequisites ?? {}, "runtimePolicy.prerequisites");
|
|
@@ -216,6 +224,9 @@ export const validateRuntimePolicy = (input) => {
|
|
|
216
224
|
triggers: Object.freeze(triggers),
|
|
217
225
|
localRows: Object.freeze(localRows),
|
|
218
226
|
expectations: Object.freeze(expectations),
|
|
227
|
+
...(Object.hasOwn(policy, "privileges")
|
|
228
|
+
? { privileges: validateRuntimePrivileges(policy.privileges) }
|
|
229
|
+
: {}),
|
|
219
230
|
});
|
|
220
231
|
};
|
|
221
232
|
export const buildRuntimePrerequisiteSql = (input) => {
|
|
@@ -360,6 +371,7 @@ export const buildRuntimeVerificationSql = (input) => {
|
|
|
360
371
|
}
|
|
361
372
|
checks.push(`if exists (select 1 from pg_constraint where not convalidated) then
|
|
362
373
|
raise exception 'Rehearsal found an unvalidated structural constraint'; end if;`);
|
|
374
|
+
checks.push(...buildRuntimePrivilegeVerificationChecks(policy.privileges ?? []));
|
|
363
375
|
return `do $rehearsal_verify$
|
|
364
376
|
begin
|
|
365
377
|
${checks.join("\n ")}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Validate opt-in effective privileges and build catalog-only verification checks. */
|
|
2
|
+
export interface RuntimePrivilegeArgumentType {
|
|
3
|
+
readonly schema: "pg_catalog";
|
|
4
|
+
readonly name: string;
|
|
5
|
+
}
|
|
6
|
+
interface RuntimePrivilegeBase {
|
|
7
|
+
readonly schema: string;
|
|
8
|
+
readonly role: string;
|
|
9
|
+
readonly required: readonly string[];
|
|
10
|
+
readonly forbidden: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
export type RuntimePrivilege = RuntimePrivilegeBase & ({
|
|
13
|
+
readonly kind: "schema";
|
|
14
|
+
} | {
|
|
15
|
+
readonly kind: "table" | "sequence";
|
|
16
|
+
readonly name: string;
|
|
17
|
+
} | {
|
|
18
|
+
readonly kind: "function";
|
|
19
|
+
readonly name: string;
|
|
20
|
+
readonly argumentTypes: readonly RuntimePrivilegeArgumentType[];
|
|
21
|
+
});
|
|
22
|
+
export declare const validateRuntimePrivileges: (input: unknown) => readonly RuntimePrivilege[];
|
|
23
|
+
/** Blocks are embedded inside the policy verifier; no ACL or object writes are emitted. */
|
|
24
|
+
export declare const buildRuntimePrivilegeVerificationChecks: (entries: readonly RuntimePrivilege[]) => readonly string[];
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/** Validate opt-in effective privileges and build catalog-only verification checks. */
|
|
2
|
+
const IDENTIFIER = /^[a-z][a-z0-9_]{0,62}$/u;
|
|
3
|
+
const TYPE_IDENTIFIER = /^[a-z_][a-z0-9_]{0,62}$/u;
|
|
4
|
+
const PRIVILEGES = {
|
|
5
|
+
schema: ["CREATE", "USAGE"],
|
|
6
|
+
table: [
|
|
7
|
+
"SELECT",
|
|
8
|
+
"INSERT",
|
|
9
|
+
"UPDATE",
|
|
10
|
+
"DELETE",
|
|
11
|
+
"TRUNCATE",
|
|
12
|
+
"REFERENCES",
|
|
13
|
+
"TRIGGER",
|
|
14
|
+
"MAINTAIN",
|
|
15
|
+
],
|
|
16
|
+
sequence: ["USAGE", "SELECT", "UPDATE"],
|
|
17
|
+
function: ["EXECUTE"],
|
|
18
|
+
};
|
|
19
|
+
const COLUMN_PRIVILEGES = new Set(["SELECT", "INSERT", "UPDATE", "REFERENCES"]);
|
|
20
|
+
const object = (value, label) => {
|
|
21
|
+
if (value === null ||
|
|
22
|
+
typeof value !== "object" ||
|
|
23
|
+
Array.isArray(value) ||
|
|
24
|
+
Object.getPrototypeOf(value) !== Object.prototype) {
|
|
25
|
+
throw new Error(`${label} must be an object.`);
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
};
|
|
29
|
+
const keys = (value, allowed, label) => {
|
|
30
|
+
if (Object.keys(value).some((key) => !allowed.includes(key)))
|
|
31
|
+
throw new Error(`${label} contains unsupported fields.`);
|
|
32
|
+
};
|
|
33
|
+
const identifier = (value, label) => {
|
|
34
|
+
if (typeof value !== "string" || !IDENTIFIER.test(value))
|
|
35
|
+
throw new Error(`${label} is not a safe PostgreSQL identifier.`);
|
|
36
|
+
return value;
|
|
37
|
+
};
|
|
38
|
+
const permissions = (value, allowed, label) => {
|
|
39
|
+
if (!Array.isArray(value) ||
|
|
40
|
+
value.length > allowed.length ||
|
|
41
|
+
value.includes(undefined) ||
|
|
42
|
+
value.some((entry) => typeof entry !== "string" || !allowed.includes(entry)) ||
|
|
43
|
+
new Set(value).size !== value.length) {
|
|
44
|
+
throw new Error(`${label} must contain distinct supported privileges.`);
|
|
45
|
+
}
|
|
46
|
+
return Object.freeze([...value]);
|
|
47
|
+
};
|
|
48
|
+
export const validateRuntimePrivileges = (input) => {
|
|
49
|
+
if (!Array.isArray(input) ||
|
|
50
|
+
input.length > 1_024 ||
|
|
51
|
+
input.includes(undefined))
|
|
52
|
+
throw new Error("runtimePolicy.privileges must be an array of at most 1024 declarations.");
|
|
53
|
+
const observed = new Map();
|
|
54
|
+
return Object.freeze(input.map((value, index) => {
|
|
55
|
+
const label = `runtimePolicy.privileges[${index}]`;
|
|
56
|
+
const entry = object(value, label);
|
|
57
|
+
if (typeof entry.kind !== "string" ||
|
|
58
|
+
!Object.hasOwn(PRIVILEGES, entry.kind)) {
|
|
59
|
+
throw new Error(`${label}.kind is unsupported.`);
|
|
60
|
+
}
|
|
61
|
+
const kind = entry.kind;
|
|
62
|
+
keys(entry, [
|
|
63
|
+
"kind",
|
|
64
|
+
"schema",
|
|
65
|
+
"role",
|
|
66
|
+
"required",
|
|
67
|
+
"forbidden",
|
|
68
|
+
...(kind === "schema" ? [] : ["name"]),
|
|
69
|
+
...(kind === "function" ? ["argumentTypes"] : []),
|
|
70
|
+
], label);
|
|
71
|
+
const schema = identifier(entry.schema, `${label}.schema`);
|
|
72
|
+
const role = identifier(entry.role, `${label}.role`);
|
|
73
|
+
const allowed = PRIVILEGES[kind];
|
|
74
|
+
const required = permissions(Object.hasOwn(entry, "required") ? entry.required : [], allowed, `${label}.required`);
|
|
75
|
+
const forbidden = permissions(Object.hasOwn(entry, "forbidden") ? entry.forbidden : [], allowed, `${label}.forbidden`);
|
|
76
|
+
if (required.length + forbidden.length === 0)
|
|
77
|
+
throw new Error(`${label} must declare a required or forbidden privilege.`);
|
|
78
|
+
const base = { schema, role, required, forbidden };
|
|
79
|
+
let normalized;
|
|
80
|
+
if (kind === "schema")
|
|
81
|
+
normalized = Object.freeze({ ...base, kind });
|
|
82
|
+
else {
|
|
83
|
+
const name = identifier(entry.name, `${label}.name`);
|
|
84
|
+
if (kind !== "function")
|
|
85
|
+
normalized = Object.freeze({ ...base, kind, name });
|
|
86
|
+
else {
|
|
87
|
+
if (!Array.isArray(entry.argumentTypes) ||
|
|
88
|
+
entry.argumentTypes.length > 32 ||
|
|
89
|
+
entry.argumentTypes.includes(undefined))
|
|
90
|
+
throw new Error(`${label}.argumentTypes must be an array of at most 32 types.`);
|
|
91
|
+
const argumentTypes = entry.argumentTypes.map((value, argumentIndex) => {
|
|
92
|
+
const typeLabel = `${label}.argumentTypes[${argumentIndex}]`;
|
|
93
|
+
const type = object(value, typeLabel);
|
|
94
|
+
keys(type, ["schema", "name"], typeLabel);
|
|
95
|
+
if (type.schema !== "pg_catalog" ||
|
|
96
|
+
typeof type.name !== "string" ||
|
|
97
|
+
!TYPE_IDENTIFIER.test(type.name)) {
|
|
98
|
+
throw new Error(`${typeLabel} must name a safe pg_catalog type.`);
|
|
99
|
+
}
|
|
100
|
+
return Object.freeze({
|
|
101
|
+
schema: "pg_catalog",
|
|
102
|
+
name: type.name,
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
normalized = Object.freeze({
|
|
106
|
+
...base,
|
|
107
|
+
kind,
|
|
108
|
+
name,
|
|
109
|
+
argumentTypes: Object.freeze(argumentTypes),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const identity = JSON.stringify([
|
|
114
|
+
kind,
|
|
115
|
+
schema,
|
|
116
|
+
normalized.kind === "schema" ? null : normalized.name,
|
|
117
|
+
role,
|
|
118
|
+
normalized.kind === "function" ? normalized.argumentTypes : null,
|
|
119
|
+
]);
|
|
120
|
+
const earlier = observed.get(identity) ?? {
|
|
121
|
+
required: new Set(),
|
|
122
|
+
forbidden: new Set(),
|
|
123
|
+
};
|
|
124
|
+
if (required.some((privilege) => forbidden.includes(privilege) || earlier.forbidden.has(privilege)) ||
|
|
125
|
+
forbidden.some((privilege) => earlier.required.has(privilege))) {
|
|
126
|
+
throw new Error(`${label} has overlapping required and forbidden privileges.`);
|
|
127
|
+
}
|
|
128
|
+
required.forEach((privilege) => earlier.required.add(privilege));
|
|
129
|
+
forbidden.forEach((privilege) => earlier.forbidden.add(privilege));
|
|
130
|
+
observed.set(identity, earlier);
|
|
131
|
+
return normalized;
|
|
132
|
+
}));
|
|
133
|
+
};
|
|
134
|
+
const literal = (value) => `'${value.replaceAll("'", "''")}'`;
|
|
135
|
+
const typeLookup = (type) => `select t.oid from pg_catalog.pg_type t
|
|
136
|
+
join pg_catalog.pg_namespace n on n.oid = t.typnamespace
|
|
137
|
+
where n.nspname = 'pg_catalog' and t.typname = ${literal(type.name)}`;
|
|
138
|
+
const objectLookup = (entry) => {
|
|
139
|
+
if (entry.kind === "schema")
|
|
140
|
+
return `select n.oid into rehearsal_object from pg_catalog.pg_namespace n
|
|
141
|
+
where n.nspname = ${literal(entry.schema)};`;
|
|
142
|
+
if (entry.kind === "function")
|
|
143
|
+
return `select p.oid into rehearsal_object from pg_catalog.pg_proc p
|
|
144
|
+
join pg_catalog.pg_namespace n on n.oid = p.pronamespace
|
|
145
|
+
where n.nspname = ${literal(entry.schema)} and p.proname = ${literal(entry.name)}
|
|
146
|
+
and p.prokind = 'f' and p.pronargs = ${entry.argumentTypes.length}
|
|
147
|
+
${entry.argumentTypes.map((type, index) => `and p.proargtypes[${index}] = (${typeLookup(type)})`).join("\n ")};`;
|
|
148
|
+
return `select c.oid into rehearsal_object from pg_catalog.pg_class c
|
|
149
|
+
join pg_catalog.pg_namespace n on n.oid = c.relnamespace
|
|
150
|
+
where n.nspname = ${literal(entry.schema)} and c.relname = ${literal(entry.name)}
|
|
151
|
+
and c.relkind ${entry.kind === "sequence" ? "= 'S'" : "in ('r', 'p', 'v', 'm', 'f')"};`;
|
|
152
|
+
};
|
|
153
|
+
const permissionCheck = (entry, privilege, forbidden) => {
|
|
154
|
+
const functionName = `pg_catalog.has_${entry.kind}_privilege`;
|
|
155
|
+
const tableCheck = `${functionName}(rehearsal_role, rehearsal_object, ${literal(privilege)})`;
|
|
156
|
+
const expression = entry.kind === "table" && forbidden && COLUMN_PRIVILEGES.has(privilege)
|
|
157
|
+
? `(${tableCheck} or pg_catalog.has_any_column_privilege(rehearsal_role, rehearsal_object, ${literal(privilege)}))`
|
|
158
|
+
: tableCheck;
|
|
159
|
+
const evaluation = privilege === "MAINTAIN"
|
|
160
|
+
? `if pg_catalog.current_setting('server_version_num')::integer < 170000 then
|
|
161
|
+
rehearsal_allowed := false;
|
|
162
|
+
else
|
|
163
|
+
rehearsal_allowed := ${expression};
|
|
164
|
+
end if;`
|
|
165
|
+
: `rehearsal_allowed := ${expression};`;
|
|
166
|
+
return `${evaluation}
|
|
167
|
+
if rehearsal_allowed is null then
|
|
168
|
+
raise exception 'Rehearsal privilege object is unavailable';
|
|
169
|
+
end if;
|
|
170
|
+
if rehearsal_allowed is ${forbidden ? "true" : "false"} then
|
|
171
|
+
raise exception 'Rehearsal ${forbidden ? "forbidden privilege is present" : "required privilege is missing"}';
|
|
172
|
+
end if;`;
|
|
173
|
+
};
|
|
174
|
+
/** Blocks are embedded inside the policy verifier; no ACL or object writes are emitted. */
|
|
175
|
+
export const buildRuntimePrivilegeVerificationChecks = (entries) => entries.map((entry) => `declare
|
|
176
|
+
rehearsal_role pg_catalog.oid;
|
|
177
|
+
rehearsal_object pg_catalog.oid;
|
|
178
|
+
rehearsal_allowed boolean;
|
|
179
|
+
begin
|
|
180
|
+
select r.oid into rehearsal_role from pg_catalog.pg_roles r
|
|
181
|
+
where r.rolname = ${literal(entry.role)};
|
|
182
|
+
if rehearsal_role is null then
|
|
183
|
+
raise exception 'Rehearsal privilege role is missing';
|
|
184
|
+
end if;
|
|
185
|
+
${entry.kind === "function"
|
|
186
|
+
? entry.argumentTypes
|
|
187
|
+
.map((type) => `if not exists (${typeLookup(type)}) then
|
|
188
|
+
raise exception 'Rehearsal privilege argument type is missing';
|
|
189
|
+
end if;`)
|
|
190
|
+
.join("\n ")
|
|
191
|
+
: ""}
|
|
192
|
+
${objectLookup(entry)}
|
|
193
|
+
if rehearsal_object is null then
|
|
194
|
+
raise exception 'Rehearsal privilege object is missing';
|
|
195
|
+
end if;
|
|
196
|
+
${entry.required.map((privilege) => permissionCheck(entry, privilege, false)).join("\n ")}
|
|
197
|
+
${entry.forbidden.map((privilege) => permissionCheck(entry, privilege, true)).join("\n ")}
|
|
198
|
+
end;`);
|
|
@@ -426,6 +426,7 @@ const resetRuntime = async () => {
|
|
|
426
426
|
path: runtimeMarkerPath,
|
|
427
427
|
marker: {
|
|
428
428
|
formatVersion: active.baseline.formatVersion,
|
|
429
|
+
target: "supabase",
|
|
429
430
|
generationId: active.baseline.generationId,
|
|
430
431
|
dataSha256: readBaselineDataSha256(active.baseline),
|
|
431
432
|
schemaSha256: active.baseline.files?.["production-schema.sql"]?.sha256 ?? null,
|
package/docs/README.md
CHANGED
|
@@ -41,7 +41,7 @@ accomplish and follow that path.
|
|
|
41
41
|
## Follow the project
|
|
42
42
|
|
|
43
43
|
- [Roadmap](roadmap.md)
|
|
44
|
-
- [Stability and
|
|
44
|
+
- [Stability and release verification](stability.md)
|
|
45
45
|
- [Repository architecture](architecture.md)
|
|
46
46
|
- [Release process](releasing.md)
|
|
47
47
|
|
package/docs/configuration.md
CHANGED
|
@@ -229,6 +229,10 @@ approval sequence.
|
|
|
229
229
|
|
|
230
230
|
`runtimePolicy` replaces common restore adapters with reviewed schemas, allowlisted
|
|
231
231
|
extensions, managed triggers, local-only singleton rows, and structural expectations.
|
|
232
|
+
The [runtime policy guide](runtime-policies.md#explicit-data-api-grants) also owns the
|
|
233
|
+
least-privilege Data API migration checklist and core 1.1.0's optional read-only
|
|
234
|
+
required/forbidden privilege declarations. Earlier releases do not accept that new
|
|
235
|
+
declaration. Rehearsal never infers or automatically grants API access.
|
|
232
236
|
`identityPolicy` declares a hashed verified-email or provider-subject matcher, copied
|
|
233
237
|
placeholder, safe signup defaults, required role/reference transfers, Storage path
|
|
234
238
|
rewrites, and token checks. Provider credentials stay in the local Supabase runtime
|
|
@@ -300,8 +304,10 @@ Rehearsal stacks.
|
|
|
300
304
|
`keep-until-stop` preserves today's interactive behavior, while `stop-after-run` stops
|
|
301
305
|
the targets after proofs, proof failures, or interruption. `lifecycle.open` uses
|
|
302
306
|
`keep-until-stop` or `stop-on-application-exit` after the app closes. Every mode stops
|
|
303
|
-
only Rehearsal's configured targets
|
|
304
|
-
|
|
307
|
+
only Rehearsal's configured targets; lifecycle selection does not enable engine shutdown.
|
|
308
|
+
The separately opted-in `stopOwnedColimaWhenIdle` policy above is the only guarded Colima
|
|
309
|
+
shutdown path. Ordinary stopping keeps database and Storage volumes and all sandbox
|
|
310
|
+
edits; it does not reset or discard them.
|
|
305
311
|
See [Container runtime lifecycle](runtime-lifecycle.md).
|
|
306
312
|
|
|
307
313
|
### Foreground application idle shutdown
|
|
@@ -637,8 +643,17 @@ The VS Code Local Resources manager reviews an exact diff before changing either
|
|
|
637
643
|
See [Local Resources](runtime-lifecycle.md#local-resources-manager) for image and volume rules.
|
|
638
644
|
|
|
639
645
|
Use unique, non-privileged ports that do not overlap. `projectId` accepts lowercase
|
|
640
|
-
letters, numbers, and hyphens
|
|
641
|
-
only this project.
|
|
646
|
+
letters, numbers, and hyphens: 2–40 characters for Supabase, or 2–63 for PostgreSQL.
|
|
647
|
+
It labels the local Docker resources so cleanup targets only this exact project.
|
|
648
|
+
Overlong configured IDs are refused before a provider starts; Rehearsal never truncates
|
|
649
|
+
them or adopts resources using a shortened prefix. Review existing resources before
|
|
650
|
+
changing a configured ID, and use a distinct reviewed identity for a new disposable
|
|
651
|
+
runtime rather than assuming that a renamed config owns old containers or volumes.
|
|
652
|
+
|
|
653
|
+
For new setup only, the generated `<project-name>-rehearsal` ID stays unchanged when it
|
|
654
|
+
fits the selected provider. Longer new IDs use a readable prefix and a deterministic
|
|
655
|
+
hash suffix within that provider's limit. Setup uses the same exact ID in the generated
|
|
656
|
+
Rehearsal config and Supabase TOML. Existing configuration files are never rewritten.
|
|
642
657
|
|
|
643
658
|
Application and readiness URLs must use a different port from the configured database,
|
|
644
659
|
API, and Studio services. Editing a URL does not rewrite your framework's development
|
package/docs/getting-started.md
CHANGED
|
@@ -12,17 +12,17 @@ Separate setup folders are available for
|
|
|
12
12
|
[macOS](https://github.com/Ddupasquier/rehearsal-db/tree/main/installers/macos) and
|
|
13
13
|
[Linux](https://github.com/Ddupasquier/rehearsal-db/tree/main/installers/linux).
|
|
14
14
|
They are readable helpers, not native executables or bundled Node/Docker installers.
|
|
15
|
-
Existing Node.js 24 or 26 is required. The default only checks an existing project;
|
|
15
|
+
Existing Node.js 22, 24 or 26 is required. The default only checks an existing project;
|
|
16
16
|
installation needs an explicit option and does not start a database. Review the
|
|
17
17
|
[shared behavior and release availability](https://github.com/Ddupasquier/rehearsal-db/blob/main/installers/README.md)
|
|
18
|
-
before choosing an install option. An exact core 1.0
|
|
18
|
+
before choosing an install option. An exact core 1.1.0 install requires that version to
|
|
19
19
|
be published; a checked-in helper is not proof of publication.
|
|
20
20
|
|
|
21
21
|
### Windows: use a WSL 2 Linux workspace
|
|
22
22
|
|
|
23
23
|
The Windows compatibility route is Linux execution inside **WSL 2**, not a native
|
|
24
|
-
PowerShell/Command Prompt database runtime.
|
|
25
|
-
boundary explicit; native Windows version/help/discovery do not authorize runtime work.
|
|
24
|
+
PowerShell/Command Prompt database runtime. Since core 1.0.1 and extension 0.1.21, that
|
|
25
|
+
boundary is explicit; native Windows version/help/discovery do not authorize runtime work.
|
|
26
26
|
|
|
27
27
|
1. Follow Microsoft's [WSL installation guide](https://learn.microsoft.com/en-us/windows/wsl/install)
|
|
28
28
|
to prepare a WSL 2 Linux distribution, such as Ubuntu 24.04. Do not use WSL 1.
|
package/docs/releasing.md
CHANGED
|
@@ -138,7 +138,9 @@ release-source migration.
|
|
|
138
138
|
4. When an unfamiliar developer is available, have them follow clean-project onboarding.
|
|
139
139
|
Correct and retest the first confusing, missing, or wrong instruction. Record when
|
|
140
140
|
this human observation was unavailable; automated checks must not impersonate it.
|
|
141
|
-
5.
|
|
141
|
+
5. Confirm package metadata keeps `private: false` for this public npm package. Any
|
|
142
|
+
metadata change belongs in the reviewed release change; do not toggle visibility
|
|
143
|
+
or package privacy as an incidental publishing step.
|
|
142
144
|
6. Record the exact tarball filename, SHA-1, SHA-256, allowlisted files, unpacked size,
|
|
143
145
|
executable, declared runtime dependency inventory, and dependency audit result.
|
|
144
146
|
7. Review the release change and its exact artifact evidence.
|
|
@@ -195,7 +197,7 @@ This is mutual exclusion, not a guaranteed ordered queue: reconcile a superseded
|
|
|
195
197
|
pending workflow before deliberately requesting another run.
|
|
196
198
|
|
|
197
199
|
The objective promotion checklist and compatibility boundary live in
|
|
198
|
-
[Stability and
|
|
200
|
+
[Stability and release verification](stability.md).
|
|
199
201
|
|
|
200
202
|
The trusted publisher allows `npm dist-tag` only so this workflow can maintain those two
|
|
201
203
|
tags without a stored npm token. A manual workflow run from `main` can repair the correct
|
package/docs/roadmap.md
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
# Next steps
|
|
2
2
|
|
|
3
|
-
Rehearsal core **1.
|
|
3
|
+
Rehearsal core **1.1.0 is available on
|
|
4
4
|
[npm](https://www.npmjs.com/package/@rehearsal-db/core)** for guided Supabase and
|
|
5
|
-
ordinary PostgreSQL migration rehearsals. The [free VS Code Starter preview](vscode.md)
|
|
6
|
-
adds a visual workbench
|
|
5
|
+
ordinary PostgreSQL migration rehearsals. The [free VS Code Starter 0.1.23 preview](vscode.md)
|
|
6
|
+
adds a visual workbench and is distributed through Marketplace and reviewed GitHub
|
|
7
|
+
VSIX releases. Neither requires a purchase or account. Core is MIT-licensed; all
|
|
8
|
+
features included in Starter are free indefinitely for personal or commercial development.
|
|
9
|
+
|
|
10
|
+
Stable core, the editor preview and a future paid Pro offer have separate completion
|
|
11
|
+
boundaries. A downloadable preview does not imply that every native accessibility,
|
|
12
|
+
provider or real-user acceptance observation has been completed. Publication does not
|
|
13
|
+
authorize a new database operation or migrate a running legacy extension.
|
|
7
14
|
|
|
8
15
|
The standalone workflow keeps the rehearsal engine in the package while application
|
|
9
16
|
meaning and privacy decisions remain project-owned. The sequence below explains its
|
|
@@ -30,10 +37,18 @@ Track the full breakdown in
|
|
|
30
37
|
For questions or feature requests, use
|
|
31
38
|
[GitHub issues](https://github.com/Ddupasquier/rehearsal-db/issues).
|
|
32
39
|
|
|
33
|
-
##
|
|
40
|
+
## Verification and supported-journey work
|
|
41
|
+
|
|
42
|
+
The [release verification policy](stability.md) owns promotion requirements. The
|
|
43
|
+
[Environment Workbench epic](https://github.com/Ddupasquier/rehearsal-db/issues/130)
|
|
44
|
+
and its children track remaining editor acceptance; the issue tracker owns current
|
|
45
|
+
status and pickup order, not this page. Supported-journey compatibility work,
|
|
46
|
+
including the [Supabase grant-preservation review](https://github.com/Ddupasquier/rehearsal-db/issues/199),
|
|
47
|
+
is distinct from adding new database families or paid features.
|
|
34
48
|
|
|
35
|
-
|
|
36
|
-
|
|
49
|
+
For each exact candidate, retain the checks already proved and complete the missing
|
|
50
|
+
observations. The following describe evidence boundaries, not a claim that a shipped
|
|
51
|
+
release or consumer upgrade remains unfinished:
|
|
37
52
|
|
|
38
53
|
- Install the exact packed candidate in clean Supabase and PostgreSQL consumers.
|
|
39
54
|
- Exercise two unrelated synthetic schemas, bounded large data, interruption, drift,
|
|
@@ -48,6 +63,12 @@ consumer migrations. Record the observed results for each exact candidate separa
|
|
|
48
63
|
|
|
49
64
|
## Later database targets
|
|
50
65
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
66
|
+
Additional database, runtime, identity-provider and migration-tool integrations require
|
|
67
|
+
their own reviewed capability contracts and independent acceptance. Research or a roadmap
|
|
68
|
+
ticket is not a support claim. MySQL, MongoDB, SQLite and other database families are not
|
|
69
|
+
supported by this PostgreSQL/Supabase release. They remain separate future work rather
|
|
70
|
+
than prerequisites for the current local workflow.
|
|
71
|
+
|
|
72
|
+
The complete CLI and its database/safety capabilities stay free. Optional paid Pro
|
|
73
|
+
workflows, licensing and commercial readiness remain separate owner decisions; this
|
|
74
|
+
roadmap does not approve a paywall, price, billing service or new distribution operation.
|
|
@@ -27,8 +27,8 @@ then repeat the command.
|
|
|
27
27
|
|
|
28
28
|
Setting `autoStartColima: true` is an explicit convenience opt-in. If Docker is
|
|
29
29
|
unavailable and the `colima` command exists, Rehearsal runs plain `colima start` and
|
|
30
|
-
reports it. Rehearsal does not resize Colima
|
|
31
|
-
|
|
30
|
+
reports it. Rehearsal does not resize Colima. Engine shutdown remains off unless the
|
|
31
|
+
separate ownership-guarded policy below is explicitly enabled. Starting Docker can also wake unrelated containers that use Docker's
|
|
32
32
|
`always` or `unless-stopped` restart policy. Rehearsal changes only the selected
|
|
33
33
|
Supabase stack to Docker's `no` restart policy after it starts successfully.
|
|
34
34
|
|
|
@@ -84,8 +84,9 @@ lifecycle: {
|
|
|
84
84
|
`run` accepts `keep-until-stop` or `stop-after-run`. `open` accepts
|
|
85
85
|
`keep-until-stop` or `stop-on-application-exit`. The bounded choices stop every
|
|
86
86
|
configured Rehearsal target in reverse dependency order after the command finishes,
|
|
87
|
-
fails after startup, times out, or is interrupted.
|
|
88
|
-
engine.
|
|
87
|
+
fails after startup, times out, or is interrupted. Lifecycle choices govern targets,
|
|
88
|
+
not engine ownership; they do not enable Colima shutdown. The separate
|
|
89
|
+
`stopOwnedColimaWhenIdle` policy still requires its own opt-in and ownership checks.
|
|
89
90
|
|
|
90
91
|
Stopping is non-destructive: database and Storage volumes, applied migrations, and
|
|
91
92
|
sandbox edits remain available to `start` or `open`. Only the existing explicit `reset`
|
package/docs/runtime-policies.md
CHANGED
|
@@ -66,6 +66,151 @@ identity policy. Verification accepts the configured placeholder before associat
|
|
|
66
66
|
the exact recorded local identity afterward. Rehearsal stores that mapping only in its
|
|
67
67
|
local internal schema; it does not duplicate the old role or weaken the row check.
|
|
68
68
|
|
|
69
|
+
### Explicit Data API grants
|
|
70
|
+
|
|
71
|
+
Supabase's [official announcement](https://supabase.com/changelog/45329-breaking-change-tables-not-exposed-to-data-and-graphql-api-automatically)
|
|
72
|
+
sets October 30, 2026 for changing existing projects to opt-in exposure of new
|
|
73
|
+
`public` tables. Existing table grants remain; this is not evidence that a particular
|
|
74
|
+
project's setting has already changed. Direct PostgreSQL access is a separate contract.
|
|
75
|
+
|
|
76
|
+
[Grants and RLS](https://supabase.com/docs/guides/api/securing-your-api) answer different
|
|
77
|
+
questions: a grant allows an API role to reach an object; an RLS policy decides which
|
|
78
|
+
rows that role can use. Passing an administrator SQL query or finding an RLS policy
|
|
79
|
+
does not prove that an application's Data API request will work safely.
|
|
80
|
+
|
|
81
|
+
For every new or recreated API table:
|
|
82
|
+
|
|
83
|
+
1. Review the exposed schemas and the exact operations needed by `anon`,
|
|
84
|
+
`authenticated`, and `service_role`. An internal table does not need API access.
|
|
85
|
+
2. Put explicit, least-privilege grants and RLS in that table's new migration. Remove
|
|
86
|
+
unwanted inherited defaults for the new object; do not blindly grant anonymous CRUD.
|
|
87
|
+
Check its sequences and callable functions separately, including `PUBLIC EXECUTE`.
|
|
88
|
+
3. Preserve already-applied migration files. Correct historical objects through a
|
|
89
|
+
separately reviewed forward migration, not by rewriting the baseline's prefix.
|
|
90
|
+
4. Test an empty setup and the disposable restore/reset/replay path with both permissive
|
|
91
|
+
and revoked automatic defaults. A schema dump alone is not proof of effective ACLs:
|
|
92
|
+
[Supabase's dump guidance](https://supabase.com/docs/reference/cli/supabase-db-dump#supabase-db-dump)
|
|
93
|
+
warns that target default privileges can broaden restored access.
|
|
94
|
+
5. Independently check SQL grants, RLS and actual allowed/denied Data API requests with
|
|
95
|
+
the application's roles. A successful `service_role` request does not prove ordinary
|
|
96
|
+
authenticated access, and an empty RLS-filtered response is not a grant refusal.
|
|
97
|
+
|
|
98
|
+
For example, a **new read-only authenticated API table**, created by its migration owner,
|
|
99
|
+
can deliberately declare its own boundary:
|
|
100
|
+
|
|
101
|
+
```sql
|
|
102
|
+
create table public.api_notes (
|
|
103
|
+
id bigint generated by default as identity primary key,
|
|
104
|
+
owner_id uuid not null,
|
|
105
|
+
body text not null
|
|
106
|
+
);
|
|
107
|
+
alter table public.api_notes enable row level security;
|
|
108
|
+
revoke all on table public.api_notes from public, anon, authenticated, service_role;
|
|
109
|
+
revoke all on sequence public.api_notes_id_seq from public, anon, authenticated, service_role;
|
|
110
|
+
grant select on table public.api_notes to authenticated;
|
|
111
|
+
create policy read_own_notes on public.api_notes
|
|
112
|
+
for select to authenticated using ((select auth.uid()) = owner_id);
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
This example deliberately permits no client inserts, sequence use or service-role
|
|
116
|
+
access. Choose a different reviewed boundary when the application needs those operations.
|
|
117
|
+
Apply such SQL only through your own authorized migration process; Rehearsal does not
|
|
118
|
+
change hosted grants or RLS on your behalf.
|
|
119
|
+
|
|
120
|
+
### Read-only privilege expectations (core 1.1.0 and newer)
|
|
121
|
+
|
|
122
|
+
Core 1.1.0 adds the following optional declaration. Earlier releases, including
|
|
123
|
+
1.0.2, do not accept `privileges`; install a published release containing this contract
|
|
124
|
+
before adding it. A project-owned application proof remains necessary for API/RLS behavior.
|
|
125
|
+
|
|
126
|
+
Add `privileges` to the same runtime-policy JSON to check required and forbidden
|
|
127
|
+
**effective** privileges after restore and migration. These checks issue no `GRANT`,
|
|
128
|
+
`REVOKE`, role switch or permission repair. They do not infer grants from RLS.
|
|
129
|
+
|
|
130
|
+
```json
|
|
131
|
+
{
|
|
132
|
+
"policyVersion": 1,
|
|
133
|
+
"privileges": [
|
|
134
|
+
{
|
|
135
|
+
"kind": "schema",
|
|
136
|
+
"schema": "public",
|
|
137
|
+
"role": "authenticated",
|
|
138
|
+
"required": ["USAGE"],
|
|
139
|
+
"forbidden": ["CREATE"]
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"kind": "table",
|
|
143
|
+
"schema": "public",
|
|
144
|
+
"name": "api_notes",
|
|
145
|
+
"role": "authenticated",
|
|
146
|
+
"required": ["SELECT"],
|
|
147
|
+
"forbidden": [
|
|
148
|
+
"INSERT",
|
|
149
|
+
"UPDATE",
|
|
150
|
+
"DELETE",
|
|
151
|
+
"TRUNCATE",
|
|
152
|
+
"REFERENCES",
|
|
153
|
+
"TRIGGER",
|
|
154
|
+
"MAINTAIN"
|
|
155
|
+
]
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"kind": "table",
|
|
159
|
+
"schema": "public",
|
|
160
|
+
"name": "api_notes",
|
|
161
|
+
"role": "anon",
|
|
162
|
+
"forbidden": [
|
|
163
|
+
"SELECT",
|
|
164
|
+
"INSERT",
|
|
165
|
+
"UPDATE",
|
|
166
|
+
"DELETE",
|
|
167
|
+
"TRUNCATE",
|
|
168
|
+
"REFERENCES",
|
|
169
|
+
"TRIGGER",
|
|
170
|
+
"MAINTAIN"
|
|
171
|
+
]
|
|
172
|
+
}
|
|
173
|
+
]
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Existing policy fields and structural/RLS expectations remain independent and compatible.
|
|
178
|
+
Omitting `privileges` preserves the previous behavior; an empty array adds no checks.
|
|
179
|
+
Each nonempty declaration needs at least one required or forbidden privilege. Omitted
|
|
180
|
+
lists mean empty, but explicit `null`, overlapping lists, unknown fields and unsupported
|
|
181
|
+
identifiers or privileges are rejected. At most 1,024 declarations are accepted.
|
|
182
|
+
|
|
183
|
+
| Kind | Object fields | Accepted privileges |
|
|
184
|
+
| ---------- | --------------------------------- | --------------------------------------------------------------------------------------- |
|
|
185
|
+
| `schema` | `schema`; no `name` | `USAGE`, `CREATE` |
|
|
186
|
+
| `table` | `schema`, `name` | `SELECT`, `INSERT`, `UPDATE`, `DELETE`, `TRUNCATE`, `REFERENCES`, `TRIGGER`, `MAINTAIN` |
|
|
187
|
+
| `sequence` | `schema`, `name` | `USAGE`, `SELECT`, `UPDATE` |
|
|
188
|
+
| `function` | `schema`, `name`, `argumentTypes` | `EXECUTE` |
|
|
189
|
+
|
|
190
|
+
Use exact uppercase privilege names and reviewed existing role/object names. Roles must
|
|
191
|
+
exist in `pg_roles`; declare an actual application role to check access inherited from
|
|
192
|
+
`PUBLIC`, not a standalone `PUBLIC` pseudo-role. Function checks cover ordinary functions,
|
|
193
|
+
not procedures, aggregates or window routines. A function declaration must include its
|
|
194
|
+
exact input types (including `INOUT`, excluding output-only arguments) as structured
|
|
195
|
+
catalog names, for example
|
|
196
|
+
`"argumentTypes": [{ "schema": "pg_catalog", "name": "uuid" }]`; `[]` selects a
|
|
197
|
+
zero-argument function. At most 32 arguments are supported. SQL signature strings and
|
|
198
|
+
non-`pg_catalog` argument types are not accepted; use a project-owned proof for unsupported
|
|
199
|
+
signatures rather than changing a function to fit the check.
|
|
200
|
+
|
|
201
|
+
Effective checks include inherited role permissions and grants through `PUBLIC`.
|
|
202
|
+
Required table access means a full-table privilege. Forbidden `SELECT`, `INSERT`,
|
|
203
|
+
`UPDATE` and `REFERENCES` also detect a privilege granted on only one column.
|
|
204
|
+
`MAINTAIN` is checked on PostgreSQL 17 and newer; below 17 it is unavailable, so a required
|
|
205
|
+
check fails and a forbidden check passes. Missing roles, objects, types or exact function
|
|
206
|
+
signatures fail rather than being interpreted as a successful denial.
|
|
207
|
+
|
|
208
|
+
These declarations do not inventory undeclared objects, prove RLS expressions or grant
|
|
209
|
+
options, change Data API exposure settings, or make a live HTTP request. Keep your
|
|
210
|
+
application's positive and negative API proof alongside them, including fresh setup and
|
|
211
|
+
replay coverage. A failed check needs a fresh reviewed migration; it is never an
|
|
212
|
+
automatic privilege escalation or a reason to use hosted administrator credentials.
|
|
213
|
+
|
|
69
214
|
## Identity policy
|
|
70
215
|
|
|
71
216
|
The identity policy links one verified local Supabase identity to a copied pseudonymous
|
package/docs/stability.md
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
# Stability and
|
|
1
|
+
# Stability and release verification
|
|
2
2
|
|
|
3
|
-
Rehearsal core **1.
|
|
3
|
+
Rehearsal core **1.1.0 is published** on
|
|
4
4
|
[npm](https://www.npmjs.com/package/@rehearsal-db/core). This page defines its
|
|
5
5
|
compatibility promise and the verification policy for releases; it is not an
|
|
6
6
|
unfinished launch checklist. Stable core does not require every planned database,
|
|
7
7
|
editor interface, or paid feature.
|
|
8
8
|
|
|
9
|
+
The compatibility promise began with **1.0.0**. The separate
|
|
10
|
+
[VS Code Starter 0.1.23 preview](vscode.md) has its own editor acceptance and does
|
|
11
|
+
not turn a core release into a completed accessibility or provider walkthrough.
|
|
12
|
+
|
|
9
13
|
## What becomes a compatibility promise
|
|
10
14
|
|
|
11
15
|
Starting with `1.0.0`, Rehearsal treats these documented surfaces as public:
|
|
@@ -65,7 +69,7 @@ ready. The sequence below is policy, not a claim that its checks have passed.
|
|
|
65
69
|
corrected candidate. Reuse sufficient unchanged evidence with its original scope;
|
|
66
70
|
do not relabel old or unavailable manual checks as new results. No minimum number
|
|
67
71
|
of calendar days replaces these checks.
|
|
68
|
-
6. Publish
|
|
72
|
+
6. Publish the selected stable version only when no blocking issue remains and the exact tarball has passed
|
|
69
73
|
the required release checks and consumer acceptance. The protected GitHub npm
|
|
70
74
|
review is the single human authorization for its displayed exact version and
|
|
71
75
|
checksum; no separate chat approval or pasted phrase is required.
|
|
@@ -94,7 +98,7 @@ baseline. Automatic compatible updates do not cross the 0.x-to-1.x version bound
|
|
|
94
98
|
review the changelog and deliberately select the stable version:
|
|
95
99
|
|
|
96
100
|
```bash
|
|
97
|
-
npm install --save-dev --save-exact @rehearsal-db/core@1.
|
|
101
|
+
npm install --save-dev --save-exact @rehearsal-db/core@1.1.0
|
|
98
102
|
npx --no-install rehearsal --version
|
|
99
103
|
npx --no-install rehearsal doctor
|
|
100
104
|
```
|
package/docs/vscode.md
CHANGED
|
@@ -4,9 +4,9 @@ The Rehearsal extension turns the local CLI into a project-aware database-change
|
|
|
4
4
|
workbench. It is for developers who want the safety of exact migration plans and disposable
|
|
5
5
|
local runtimes without memorizing every command.
|
|
6
6
|
|
|
7
|
-
This guide targets **Free Starter 0.1.
|
|
7
|
+
This guide targets **Free Starter 0.1.23**, named **Rehearsal DB** on
|
|
8
8
|
[VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=rehearsal-db.rehearsal-db),
|
|
9
|
-
with a [reviewed GitHub VSIX](https://github.com/Ddupasquier/rehearsal-vscode/releases/tag/v0.1.
|
|
9
|
+
with a [reviewed GitHub VSIX](https://github.com/Ddupasquier/rehearsal-vscode/releases/tag/v0.1.23)
|
|
10
10
|
for direct installation. The canonical extension ID is **`rehearsal-db.rehearsal-db`**,
|
|
11
11
|
published by **`rehearsal-db`**. The workbench and commands retain the **Rehearsal** brand.
|
|
12
12
|
All included features are free indefinitely for personal or commercial development.
|
|
@@ -44,8 +44,8 @@ boundaries, baselines, migration digests, runtime isolation, locks, rollback, an
|
|
|
44
44
|
Use desktop VS Code 1.101 or newer on macOS or Linux and a supported Node.js release
|
|
45
45
|
(22, 24, or 26). You do not need the extension source repository or Test Lab.
|
|
46
46
|
For a Windows desktop, use the [WSL 2 Linux-workspace setup](getting-started.md#windows-use-a-wsl-2-linux-workspace).
|
|
47
|
-
|
|
48
|
-
process and
|
|
47
|
+
Since editor 0.1.21, native Windows inspection and foreground actions refuse before launching a child
|
|
48
|
+
process and direct you to Remote-WSL. Install the extension into the Linux extension
|
|
49
49
|
host and keep the project on its Linux filesystem. WSL compatibility remains a preview;
|
|
50
50
|
real WSL core fixtures do not certify a native Remote-WSL editor walkthrough.
|
|
51
51
|
|
|
@@ -66,7 +66,7 @@ real WSL core fixtures do not certify a native Remote-WSL editor walkthrough.
|
|
|
66
66
|
4. Follow **Next best step**. Review the detected project and active configuration;
|
|
67
67
|
setup, source access, migration execution and cleanup keep their own confirmations.
|
|
68
68
|
|
|
69
|
-
The release pairing described here is core **1.0.2** with Starter **0.1.
|
|
69
|
+
The release pairing described here is core **1.0.2** with Starter **0.1.23**.
|
|
70
70
|
Install it once both versions appear in their official release channels.
|
|
71
71
|
The extension's minimum core is `0.1.0-rc.4`. A project that is not ready should show
|
|
72
72
|
setup guidance, not require you to build the extension. Opening the dashboard does not
|
|
@@ -92,10 +92,10 @@ distinction between desktop OS, extension host, architecture and actual database
|
|
|
92
92
|
### Direct GitHub installation
|
|
93
93
|
|
|
94
94
|
The verified GitHub VSIX is an alternative to Marketplace. Open the
|
|
95
|
-
[Starter 0.1.
|
|
96
|
-
Under **Assets**, download `rehearsal-db-0.1.
|
|
97
|
-
run `shasum -a 256 /path/to/rehearsal-db-0.1.
|
|
98
|
-
`sha256sum /path/to/rehearsal-db-0.1.
|
|
95
|
+
[Starter 0.1.23 release](https://github.com/Ddupasquier/rehearsal-vscode/releases/tag/v0.1.23).
|
|
96
|
+
Under **Assets**, download `rehearsal-db-0.1.23.vsix` and `SHA256SUMS`. In a terminal,
|
|
97
|
+
run `shasum -a 256 /path/to/rehearsal-db-0.1.23.vsix` on macOS or
|
|
98
|
+
`sha256sum /path/to/rehearsal-db-0.1.23.vsix` on Linux. Compare the result with that
|
|
99
99
|
release's `SHA256SUMS`; stop if they differ. Do not substitute a local rebuild.
|
|
100
100
|
Then open **Extensions → … → Install from VSIX…** and choose the verified file.
|
|
101
101
|
Save your work and reload only if VS Code asks.
|
|
@@ -147,6 +147,11 @@ reviewed change. Wait for **Saved**, then choose **Return to project**. Concurre
|
|
|
147
147
|
refuse safely instead of overwriting someone else's work; a saved confirmation is tied
|
|
148
148
|
to the exact bytes read back from disk, not merely a completed write request.
|
|
149
149
|
|
|
150
|
+
Invalid structured fields show supported values or numeric limits before review.
|
|
151
|
+
For example, retained environment count must be a whole number from 1 to 1000.
|
|
152
|
+
If a review becomes stale, close it, refresh Rehearsal, and review the current source;
|
|
153
|
+
saved and unsaved edits are not reverted. A refusal does not retry or reset a database.
|
|
154
|
+
|
|
150
155
|
Commands reuse the same Rehearsal terminal for a project only after its previous command
|
|
151
156
|
has a confirmed exit and its directory still matches. A running sandbox or manually
|
|
152
157
|
entered foreground command is never interrupted to make room for another action. If
|
|
@@ -222,7 +227,7 @@ route; do not delete its coordination lock while a package manager may still be
|
|
|
222
227
|
|
|
223
228
|
Manual updating stays available: download and verify the next reviewed VSIX, then repeat
|
|
224
229
|
**Install from VSIX…**. See the release's
|
|
225
|
-
[installation guide](https://github.com/Ddupasquier/rehearsal-vscode/blob/v0.1.
|
|
230
|
+
[installation guide](https://github.com/Ddupasquier/rehearsal-vscode/blob/v0.1.23/README.md#install-and-open-your-project).
|
|
226
231
|
An ordinary core-package update is detected from its manifest, lockfile and installed
|
|
227
232
|
metadata without reloading VS Code. The header shows loaded extension and core versions
|
|
228
233
|
separately; installed → newest appears only after a successful release check. Unknown or
|
|
@@ -239,8 +244,8 @@ external links may use their own services.
|
|
|
239
244
|
|
|
240
245
|
## License, support and future Pro
|
|
241
246
|
|
|
242
|
-
Starter 0.1.
|
|
243
|
-
[free Starter license](https://github.com/Ddupasquier/rehearsal-vscode/blob/v0.1.
|
|
247
|
+
Starter 0.1.23 carries the same
|
|
248
|
+
[free Starter license](https://github.com/Ddupasquier/rehearsal-vscode/blob/v0.1.23/LICENSE).
|
|
244
249
|
Its current review, configuration, comparison and reporting tools are included; they
|
|
245
250
|
are not a timed evaluation or a paid feature gate. Free use does not mean the extension
|
|
246
251
|
is MIT-licensed: its bundled terms govern copying and redistribution. Core remains MIT.
|
|
@@ -249,17 +254,17 @@ Future optional Pro editor workflows must be separately identified. They cannot
|
|
|
249
254
|
retroactively remove Starter rights already granted for a received version. Pro pricing,
|
|
250
255
|
billing and entitlements are not implemented; no payment is needed to use Starter.
|
|
251
256
|
Database and safety capabilities stay in the free CLI. The
|
|
252
|
-
[edition guide](https://github.com/Ddupasquier/rehearsal-vscode/blob/v0.1.
|
|
257
|
+
[edition guide](https://github.com/Ddupasquier/rehearsal-vscode/blob/v0.1.23/docs/commercial-readiness.md)
|
|
253
258
|
owns the detailed boundary.
|
|
254
259
|
|
|
255
260
|
Preview describes stability, not a trial countdown. Support is best effort, with no
|
|
256
261
|
service-level agreement, database hosting, backups or production support. Before use,
|
|
257
|
-
read the release's [privacy statement](https://github.com/Ddupasquier/rehearsal-vscode/blob/v0.1.
|
|
258
|
-
and [support policy](https://github.com/Ddupasquier/rehearsal-vscode/blob/v0.1.
|
|
262
|
+
read the release's [privacy statement](https://github.com/Ddupasquier/rehearsal-vscode/blob/v0.1.23/PRIVACY.md)
|
|
263
|
+
and [support policy](https://github.com/Ddupasquier/rehearsal-vscode/blob/v0.1.23/SUPPORT.md).
|
|
259
264
|
Report editor problems in the
|
|
260
265
|
[Rehearsal issue tracker](https://github.com/Ddupasquier/rehearsal-db/issues), as
|
|
261
266
|
described in the support policy.
|
|
262
|
-
Use its [private security process](https://github.com/Ddupasquier/rehearsal-vscode/blob/v0.1.
|
|
267
|
+
Use its [private security process](https://github.com/Ddupasquier/rehearsal-vscode/blob/v0.1.23/SECURITY.md)
|
|
263
268
|
for vulnerabilities; never post credentials, private rows or baseline files publicly.
|
|
264
269
|
|
|
265
270
|
Historical VSIX files retain their bundled terms. Choose a reviewed release carrying
|
package/package.json
CHANGED