-
Notifications
You must be signed in to change notification settings - Fork 0
feat(plugin): persist tenant-scoped installation authority [superseded by #169] #156
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
31 commits
Select commit
Hold shift + click to select a range
de3b919
test(plugin): define durable installation store contract
seonghobae ef6a033
test(plugin): define installation persistence migration
seonghobae 8426598
feat(plugin): add installation authority migration
seonghobae 3484b96
feat(plugin): persist installation authority atomically
seonghobae 886a148
test(plugin): preserve durable timestamp on idempotent replay
seonghobae 8c9b7d5
fix(plugin): keep original timestamp on installation replay
seonghobae 89401ed
docs(plugin): trace durable installation persistence
seonghobae 8032f80
test(plugin): require workspace-scoped installation lookup
seonghobae c3e19aa
fix(plugin): keep installation lookup tenant-scoped in persistence
seonghobae 6446b21
test(plugin): require workspace predicate for installation lookup
seonghobae 35b6ac7
fix(plugin): scope persisted installation reads by workspace
seonghobae 2e57e3f
test(plugin): require installer user authority for lookup and revoke
seonghobae c2a1c4e
Merge branch 'main' into feat/plugin-installation-persistence
github-actions[bot] d0e9aee
fix(plugin): bind installation authority to installer user
seonghobae acc2151
fix(plugin): bind durable installation authority to installer
seonghobae 2cd0344
test(plugin): prove installer-scoped persistence outcomes
seonghobae 06c9063
test(plugin): prove tenant and installer lookup isolation
seonghobae d4bcafe
test(plugin): exercise installation constraints in PostgreSQL
seonghobae 5d0ada8
docs(plugin): align persistence evidence with current standards
seonghobae 3a9ab93
fix(plugin): use CommonJS-safe migration path
seonghobae 1cf609c
test(plugin): reject invalid persisted capability elements
seonghobae 8089069
fix(plugin): constrain persisted capability elements
seonghobae 5356260
Merge branch 'main' into feat/plugin-installation-persistence
github-actions[bot] 90ae9c1
docs(plugin): record durable installation authority
seonghobae 674a49a
test(plugin): enforce durable installation constraints
seonghobae ab01e99
fix(plugin): enforce durable installation invariants
seonghobae b45f692
test(plugin): isolate durable installation evidence
seonghobae a952044
ci(plugin): expose dedicated integration database
seonghobae 7206b09
ci(plugin): run dedicated integration persistence tests
seonghobae 55c42f9
docs(plugin): clarify revocation authority
seonghobae 70785c1
Merge branch 'main' into feat/plugin-installation-persistence
github-actions[bot] File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
76 changes: 76 additions & 0 deletions
76
apps/integration-service/migrations/0001_plugin_installation_record.sql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| CREATE SCHEMA IF NOT EXISTS plugin_integration; | ||
|
|
||
| CREATE FUNCTION plugin_integration.capability_array_is_valid(capability_values text[]) | ||
| RETURNS boolean | ||
| LANGUAGE sql | ||
| IMMUTABLE | ||
| STRICT | ||
| PARALLEL SAFE | ||
| AS $$ | ||
| SELECT | ||
| COALESCE( | ||
| bool_and(char_length(capability_name) BETWEEN 1 AND 256), | ||
| true | ||
| ) | ||
| AND cardinality(capability_values) = ( | ||
| SELECT count(DISTINCT capability_name COLLATE "C") | ||
| FROM unnest(capability_values) AS capability_name | ||
| ) | ||
| AND capability_values = ARRAY( | ||
| SELECT capability_name | ||
| FROM unnest(capability_values) AS capability_name | ||
| ORDER BY capability_name COLLATE "C" | ||
| ); | ||
| $$; | ||
|
|
||
| CREATE TABLE plugin_integration.plugin_installation_record ( | ||
| installation_id uuid PRIMARY KEY, | ||
| workspace_id uuid NOT NULL, | ||
| installed_by_user_id uuid NOT NULL, | ||
| plugin_id text NOT NULL, | ||
| plugin_contract_version text NOT NULL, | ||
| manifest_sha256 text NOT NULL, | ||
| granted_capabilities text[] NOT NULL, | ||
| installation_status text NOT NULL DEFAULT 'active', | ||
| installed_at timestamptz NOT NULL, | ||
| revoked_at timestamptz, | ||
| CONSTRAINT plugin_installation_id_uuid_v4 CHECK ( | ||
| installation_id::text ~ '^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' | ||
| ), | ||
| CONSTRAINT plugin_installation_workspace_id_uuid_v4 CHECK ( | ||
| workspace_id::text ~ '^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' | ||
| ), | ||
| CONSTRAINT plugin_installation_user_id_uuid_v4 CHECK ( | ||
| installed_by_user_id::text ~ '^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' | ||
| ), | ||
| CONSTRAINT plugin_installation_plugin_id_length CHECK ( | ||
| char_length(plugin_id) BETWEEN 1 AND 256 | ||
| ), | ||
| CONSTRAINT plugin_installation_contract_version_length CHECK ( | ||
| char_length(plugin_contract_version) BETWEEN 1 AND 128 | ||
| ), | ||
| CONSTRAINT plugin_installation_manifest_sha256 CHECK ( | ||
| char_length(manifest_sha256) = 64 | ||
| AND manifest_sha256 ~ '^[0-9a-f]{64}$' | ||
| ), | ||
| CONSTRAINT plugin_installation_capability_count CHECK ( | ||
| cardinality(granted_capabilities) BETWEEN 0 AND 32 | ||
| ), | ||
| CONSTRAINT plugin_installation_capability_array CHECK ( | ||
| array_position(granted_capabilities, NULL) IS NULL | ||
| AND plugin_integration.capability_array_is_valid(granted_capabilities) | ||
| ), | ||
| CONSTRAINT plugin_installation_status_valid CHECK ( | ||
| installation_status IN ('active', 'revoked') | ||
| ), | ||
| CONSTRAINT plugin_installation_lifecycle_consistency CHECK ( | ||
| (revoked_at IS NULL OR revoked_at >= installed_at) | ||
| AND ( | ||
| (installation_status = 'active' AND revoked_at IS NULL) | ||
| OR (installation_status = 'revoked' AND revoked_at IS NOT NULL) | ||
| ) | ||
| ) | ||
| ); | ||
|
|
||
| CREATE INDEX plugin_installation_workspace_index | ||
| ON plugin_integration.plugin_installation_record (workspace_id, installation_status); |
266 changes: 266 additions & 0 deletions
266
apps/integration-service/src/plugin-installation-migration.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,266 @@ | ||
| import { spawnSync } from 'node:child_process'; | ||
| import { readFileSync } from 'node:fs'; | ||
| import { join } from 'node:path'; | ||
| import { beforeEach, describe, expect, it } from 'vitest'; | ||
|
|
||
| const MIGRATION_PATH = join( | ||
| __dirname, | ||
| '..', | ||
| 'migrations', | ||
| '0001_plugin_installation_record.sql', | ||
| ); | ||
| const MIGRATION_SQL = readFileSync(MIGRATION_PATH, 'utf8'); | ||
| const DATABASE_URL = process.env.INTEGRATION_DATABASE_URL; | ||
| const describeWithPostgres = DATABASE_URL ? describe : describe.skip; | ||
|
|
||
| interface SqlExecution { | ||
| readonly status: number | null; | ||
| readonly stdout: string; | ||
| readonly stderr: string; | ||
| } | ||
|
|
||
| /** Executes one isolated PostgreSQL client process against the dedicated disposable integration database. */ | ||
| function executeSql(sql: string): SqlExecution { | ||
| if (!DATABASE_URL) { | ||
| throw new Error('A dedicated PostgreSQL integration test database URL is required'); | ||
| } | ||
| const target = new URL(DATABASE_URL); | ||
| const result = spawnSync( | ||
| 'psql', | ||
| [ | ||
| '-X', | ||
| '-v', | ||
| 'ON_ERROR_STOP=1', | ||
| '-h', | ||
| target.hostname, | ||
| '-p', | ||
| target.port || '5432', | ||
| '-U', | ||
| decodeURIComponent(target.username), | ||
| '-d', | ||
| decodeURIComponent(target.pathname.replace(/^\//u, '')), | ||
| '-Atq', | ||
| ], | ||
| { | ||
| input: sql, | ||
| encoding: 'utf8', | ||
| env: { | ||
| ...process.env, | ||
| PGPASSWORD: decodeURIComponent(target.password), | ||
| }, | ||
| }, | ||
| ); | ||
| if (result.error) { | ||
| throw result.error; | ||
| } | ||
| return { | ||
| status: result.status, | ||
| stdout: result.stdout, | ||
| stderr: result.stderr, | ||
| }; | ||
| } | ||
|
|
||
| /** Applies SQL and surfaces only bounded diagnostics when an expected setup fails. */ | ||
| function requireSqlSuccess(sql: string): string { | ||
| const result = executeSql(sql); | ||
| if (result.status !== 0) { | ||
| throw new Error(`PostgreSQL test setup failed: ${result.stderr.slice(0, 500)}`); | ||
| } | ||
| return result.stdout.trim(); | ||
| } | ||
|
|
||
| /** Proves that a specific PostgreSQL constraint rejects one fixed hostile fixture. */ | ||
| function expectSqlFailure(sql: string, expectedConstraint: string): void { | ||
| const result = executeSql(sql); | ||
| expect(result.status).not.toBe(0); | ||
| expect(result.stderr).toContain(expectedConstraint); | ||
| } | ||
|
|
||
| describe('plugin installation migration contract', () => { | ||
| it('uses descriptive multiword database names and stores authority evidence without secret material', () => { | ||
| expect(MIGRATION_SQL).toContain( | ||
| 'CREATE SCHEMA IF NOT EXISTS plugin_integration', | ||
| ); | ||
| expect(MIGRATION_SQL).toContain( | ||
| 'CREATE TABLE plugin_integration.plugin_installation_record', | ||
| ); | ||
| for (const column of [ | ||
| 'installation_id uuid PRIMARY KEY', | ||
| 'workspace_id uuid NOT NULL', | ||
| 'installed_by_user_id uuid NOT NULL', | ||
| 'plugin_id text NOT NULL', | ||
| 'plugin_contract_version text NOT NULL', | ||
| 'manifest_sha256 text NOT NULL', | ||
| 'granted_capabilities text[] NOT NULL', | ||
| "installation_status text NOT NULL DEFAULT 'active'", | ||
| 'installed_at timestamptz NOT NULL', | ||
| 'revoked_at timestamptz', | ||
| ]) { | ||
| expect(MIGRATION_SQL).toContain(column); | ||
| } | ||
| for (const constraint of [ | ||
| 'plugin_installation_id_uuid_v4', | ||
| 'plugin_installation_workspace_id_uuid_v4', | ||
| 'plugin_installation_user_id_uuid_v4', | ||
| 'plugin_installation_manifest_sha256', | ||
| 'plugin_installation_capability_count', | ||
| 'plugin_installation_capability_array', | ||
| 'plugin_installation_lifecycle_consistency', | ||
| ]) { | ||
| expect(MIGRATION_SQL).toContain(`CONSTRAINT ${constraint}`); | ||
| } | ||
| expect(MIGRATION_SQL).toContain( | ||
| 'plugin_integration.capability_array_is_valid', | ||
| ); | ||
| expect(MIGRATION_SQL).not.toMatch( | ||
| /\b(secret|token|credential|password)_/iu, | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| describeWithPostgres('plugin installation PostgreSQL constraints', () => { | ||
| beforeEach(() => { | ||
| requireSqlSuccess('DROP SCHEMA IF EXISTS plugin_integration CASCADE;'); | ||
| requireSqlSuccess(MIGRATION_SQL); | ||
| }); | ||
|
|
||
| it('rejects impossible lifecycle, digest, capability, and UUID authority evidence', () => { | ||
| expectSqlFailure( | ||
| `INSERT INTO plugin_integration.plugin_installation_record ( | ||
| installation_id, workspace_id, installed_by_user_id, plugin_id, | ||
| plugin_contract_version, manifest_sha256, granted_capabilities, | ||
| installation_status, installed_at, revoked_at | ||
| ) VALUES ( | ||
| '11111111-1111-4111-8111-111111111111', | ||
| '22222222-2222-4222-8222-222222222222', | ||
| '33333333-3333-4333-8333-333333333333', | ||
| 'example.plugin', '1.0.0', repeat('a', 64), ARRAY['a'], | ||
| 'active', '2026-08-10T02:00:00.000Z', '2026-08-10T03:00:00.000Z' | ||
| );`, | ||
| 'plugin_installation_lifecycle_consistency', | ||
| ); | ||
|
|
||
| expectSqlFailure( | ||
| `INSERT INTO plugin_integration.plugin_installation_record ( | ||
| installation_id, workspace_id, installed_by_user_id, plugin_id, | ||
| plugin_contract_version, manifest_sha256, granted_capabilities, | ||
| installation_status, installed_at, revoked_at | ||
| ) VALUES ( | ||
| '11111111-1111-4111-8111-111111111112', | ||
| '22222222-2222-4222-8222-222222222222', | ||
| '33333333-3333-4333-8333-333333333333', | ||
| 'example.plugin', '1.0.0', repeat('a', 63), ARRAY['a'], | ||
| 'active', '2026-08-10T02:00:00.000Z', NULL | ||
| );`, | ||
| 'plugin_installation_manifest_sha256', | ||
| ); | ||
|
|
||
| expectSqlFailure( | ||
| `INSERT INTO plugin_integration.plugin_installation_record ( | ||
| installation_id, workspace_id, installed_by_user_id, plugin_id, | ||
| plugin_contract_version, manifest_sha256, granted_capabilities, | ||
| installation_status, installed_at, revoked_at | ||
| ) VALUES ( | ||
| '11111111-1111-4111-8111-111111111113', | ||
| '22222222-2222-4222-8222-222222222222', | ||
| '33333333-3333-4333-8333-333333333333', | ||
| 'example.plugin', '1.0.0', repeat('a', 64), | ||
| ARRAY(SELECT 'capability.' || lpad(value::text, 2, '0') | ||
| FROM generate_series(1, 33) AS value ORDER BY value), | ||
| 'active', '2026-08-10T02:00:00.000Z', NULL | ||
| );`, | ||
| 'plugin_installation_capability_count', | ||
| ); | ||
|
|
||
| for (const fixture of [ | ||
| { | ||
| sql: `INSERT INTO plugin_integration.plugin_installation_record | ||
| VALUES ('11111111-1111-4111-8111-111111111114', '22222222-2222-4222-8222-222222222222', | ||
| '33333333-3333-4333-8333-333333333333', 'example.plugin', '1.0.0', repeat('a', 64), | ||
| ARRAY[''], 'active', '2026-08-10T02:00:00.000Z', NULL);`, | ||
| constraint: 'plugin_installation_capability_array', | ||
| }, | ||
| { | ||
| sql: `INSERT INTO plugin_integration.plugin_installation_record | ||
| VALUES ('11111111-1111-4111-8111-111111111115', '22222222-2222-4222-8222-222222222222', | ||
| '33333333-3333-4333-8333-333333333333', 'example.plugin', '1.0.0', repeat('a', 64), | ||
| ARRAY[NULL::text], 'active', '2026-08-10T02:00:00.000Z', NULL);`, | ||
| constraint: 'plugin_installation_capability_array', | ||
| }, | ||
| { | ||
| sql: `INSERT INTO plugin_integration.plugin_installation_record | ||
| VALUES ('11111111-1111-4111-8111-111111111116', '22222222-2222-4222-8222-222222222222', | ||
| '33333333-3333-4333-8333-333333333333', 'example.plugin', '1.0.0', repeat('a', 64), | ||
| ARRAY[repeat('x', 257)], 'active', '2026-08-10T02:00:00.000Z', NULL);`, | ||
| constraint: 'plugin_installation_capability_array', | ||
| }, | ||
| { | ||
| sql: `INSERT INTO plugin_integration.plugin_installation_record | ||
| VALUES ('11111111-1111-7111-8111-111111111117', '22222222-2222-4222-8222-222222222222', | ||
| '33333333-3333-4333-8333-333333333333', 'example.plugin', '1.0.0', repeat('a', 64), | ||
| ARRAY['a'], 'active', '2026-08-10T02:00:00.000Z', NULL);`, | ||
| constraint: 'plugin_installation_id_uuid_v4', | ||
| }, | ||
| { | ||
| sql: `INSERT INTO plugin_integration.plugin_installation_record | ||
| VALUES ('11111111-1111-4111-8111-111111111118', '22222222-2222-7222-8222-222222222222', | ||
| '33333333-3333-4333-8333-333333333333', 'example.plugin', '1.0.0', repeat('a', 64), | ||
| ARRAY['a'], 'active', '2026-08-10T02:00:00.000Z', NULL);`, | ||
| constraint: 'plugin_installation_workspace_id_uuid_v4', | ||
| }, | ||
| { | ||
| sql: `INSERT INTO plugin_integration.plugin_installation_record | ||
| VALUES ('11111111-1111-4111-8111-111111111119', '22222222-2222-4222-8222-222222222222', | ||
| '33333333-3333-7333-8333-333333333333', 'example.plugin', '1.0.0', repeat('a', 64), | ||
| ARRAY['a'], 'active', '2026-08-10T02:00:00.000Z', NULL);`, | ||
| constraint: 'plugin_installation_user_id_uuid_v4', | ||
| }, | ||
| { | ||
| sql: `INSERT INTO plugin_integration.plugin_installation_record | ||
| VALUES ('11111111-1111-4111-8111-111111111120', '22222222-2222-4222-8222-222222222222', | ||
| '33333333-3333-4333-8333-333333333333', 'example.plugin', '1.0.0', repeat('a', 64), | ||
| ARRAY['a', 'a'], 'active', '2026-08-10T02:00:00.000Z', NULL);`, | ||
| constraint: 'plugin_installation_capability_array', | ||
| }, | ||
| { | ||
| sql: `INSERT INTO plugin_integration.plugin_installation_record | ||
| VALUES ('11111111-1111-4111-8111-111111111121', '22222222-2222-4222-8222-222222222222', | ||
| '33333333-3333-4333-8333-333333333333', 'example.plugin', '1.0.0', repeat('a', 64), | ||
| ARRAY['b', 'a'], 'active', '2026-08-10T02:00:00.000Z', NULL);`, | ||
| constraint: 'plugin_installation_capability_array', | ||
| }, | ||
| ] as const) { | ||
| expectSqlFailure(fixture.sql, fixture.constraint); | ||
| } | ||
|
|
||
| requireSqlSuccess(`INSERT INTO plugin_integration.plugin_installation_record | ||
| VALUES ('11111111-1111-4111-8111-111111111122', '22222222-2222-4222-8222-222222222222', | ||
| '33333333-3333-4333-8333-333333333333', 'example.plugin', '1.0.0', repeat('a', 64), | ||
| ARRAY['a', 'b'], 'active', '2026-08-10T02:00:00.000Z', NULL);`); | ||
| }); | ||
|
|
||
| it('preserves one durable authority row across independent PostgreSQL client processes', () => { | ||
| requireSqlSuccess(` | ||
| INSERT INTO plugin_integration.plugin_installation_record ( | ||
| installation_id, workspace_id, installed_by_user_id, plugin_id, | ||
| plugin_contract_version, manifest_sha256, granted_capabilities, | ||
| installation_status, installed_at, revoked_at | ||
| ) VALUES ( | ||
| '11111111-1111-4111-8111-111111111111', | ||
| '22222222-2222-4222-8222-222222222222', | ||
| '33333333-3333-4333-8333-333333333333', | ||
| 'example.plugin', '1.0.0', repeat('a', 64), ARRAY['a'], | ||
| 'active', '2026-08-10T02:00:00.000Z', NULL | ||
| ); | ||
| `); | ||
|
|
||
| const durable = requireSqlSuccess(` | ||
| SELECT installation_id || '|' || workspace_id || '|' || installed_by_user_id || '|' || installation_status | ||
| FROM plugin_integration.plugin_installation_record | ||
| WHERE installation_id = '11111111-1111-4111-8111-111111111111'::uuid; | ||
| `); | ||
| expect(durable).toBe( | ||
| '11111111-1111-4111-8111-111111111111|22222222-2222-4222-8222-222222222222|33333333-3333-4333-8333-333333333333|active', | ||
| ); | ||
| }); | ||
| }); | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.