From e2fda5437f3be9a19acf15ccd89462cbb2a6ac93 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Tue, 4 Aug 2026 23:47:09 +0200 Subject: [PATCH 1/5] fix: serialize configured fields through JSON --- src/adapter.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/adapter.ts b/src/adapter.ts index 06e7005..b201f3e 100644 --- a/src/adapter.ts +++ b/src/adapter.ts @@ -120,7 +120,10 @@ export function createSupabaseVaultAdapter( const inserted = await executor.query( `insert into ${metadataTable} (project_id, environment, secret_ref, vault_secret_id, kind, provider, configured_fields) - values ($1, $2, $3, $4::uuid, $5, $6, $7::text[]) + values ( + $1, $2, $3, $4::uuid, $5, $6, + array(select jsonb_array_elements_text($7::jsonb)) + ) returning project_id, environment, secret_ref, kind, provider, configured_fields, created_at::text, updated_at::text`, [ @@ -130,7 +133,7 @@ export function createSupabaseVaultAdapter( vaultId, input.kind, input.provider ?? null, - Object.keys(normalized.data.payload).sort(), + serializeConfiguredFields(normalized.data.payload), ], ); @@ -164,7 +167,10 @@ export function createSupabaseVaultAdapter( const updated = await executor.query( `update ${metadataTable} - set configured_fields = $4::text[], updated_at = now() + set configured_fields = array( + select jsonb_array_elements_text($4::jsonb) + ), + updated_at = now() where project_id = $1 and environment = $2 and secret_ref = $3 returning project_id, environment, secret_ref, kind, provider, configured_fields, created_at::text, updated_at::text`, @@ -172,7 +178,7 @@ export function createSupabaseVaultAdapter( normalized.data.scope.projectId, normalized.data.scope.environment, normalized.data.ref, - Object.keys(normalized.data.payload).sort(), + serializeConfiguredFields(normalized.data.payload), ], ); @@ -302,6 +308,10 @@ function toMetadata(row: MetadataRow): SecretMetadata { }; } +function serializeConfiguredFields(payload: SecretPayload): string { + return JSON.stringify(Object.keys(payload).sort()); +} + function buildInternalName(projectId: string, environment: string, ref: string): string { return `ankhorage/${encodeURIComponent(projectId)}/${encodeURIComponent(environment)}/${ref}`; } From 6fb6575618a3d3f69507c836097fb49833f5c645 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Tue, 4 Aug 2026 23:47:41 +0200 Subject: [PATCH 2/5] test: cover scalar configured field serialization --- src/adapter.test.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/adapter.test.ts b/src/adapter.test.ts index ec6b1f0..7d175e9 100644 --- a/src/adapter.test.ts +++ b/src/adapter.test.ts @@ -50,7 +50,7 @@ describe('createSupabaseVaultAdapter', () => { ref: 'auth/oauth/google', kind: 'oauth', provider: 'google', - payload: { clientId: 'public-id', clientSecret: 'SENTINEL_SECRET' }, + payload: { clientSecret: 'SENTINEL_SECRET', clientId: 'public-id' }, }); expect(result).toEqual({ @@ -67,6 +67,13 @@ describe('createSupabaseVaultAdapter', () => { }); expect(JSON.stringify(result)).not.toContain('SENTINEL_SECRET'); expect(JSON.stringify(result)).not.toContain('vault-id'); + + const insertCall = client.calls[2]; + expect(insertCall?.sql).toContain( + 'array(select jsonb_array_elements_text($7::jsonb))', + ); + expect(insertCall?.parameters[6]).toBe('["clientId","clientSecret"]'); + expect(Array.isArray(insertCall?.parameters[6])).toBe(false); }); test('scopes every lookup by project and environment', async () => { @@ -97,6 +104,11 @@ describe('createSupabaseVaultAdapter', () => { expect(result.ok).toBe(true); expect(client.calls.some((call) => call.sql.includes('decrypted_secrets'))).toBe(false); expect(JSON.stringify(result)).not.toContain('ROTATED_SECRET'); + + const updateCall = client.calls[2]; + expect(updateCall?.sql).toContain('select jsonb_array_elements_text($4::jsonb)'); + expect(updateCall?.parameters[3]).toBe('["clientSecret"]'); + expect(Array.isArray(updateCall?.parameters[3])).toBe(false); }); test('resolves payload only through the trusted resolve operation', async () => { From c39c870e42071bf7714495418f096572d24cac96 Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Tue, 4 Aug 2026 23:47:54 +0200 Subject: [PATCH 3/5] chore: add Supabase Vault array binding changeset --- .changeset/calm-ravens-store.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/calm-ravens-store.md diff --git a/.changeset/calm-ravens-store.md b/.changeset/calm-ravens-store.md new file mode 100644 index 0000000..1042ed7 --- /dev/null +++ b/.changeset/calm-ravens-store.md @@ -0,0 +1,5 @@ +--- +'@ankhorage/supabase-vault': patch +--- + +Serialize configured secret field names through JSON before reconstructing PostgreSQL text arrays, avoiding Bun malformed-array binding failures during secret creation and replacement. From 7044141308d5f9d25b28f1fc99f5023895b4e28a Mon Sep 17 00:00:00 2001 From: Fabio Gartenmann <137318798+artiphishle@users.noreply.github.com> Date: Tue, 4 Aug 2026 23:51:12 +0200 Subject: [PATCH 4/5] test: satisfy prefer-destructuring lint rule --- src/adapter.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/adapter.test.ts b/src/adapter.test.ts index 7d175e9..b91ae79 100644 --- a/src/adapter.test.ts +++ b/src/adapter.test.ts @@ -68,7 +68,7 @@ describe('createSupabaseVaultAdapter', () => { expect(JSON.stringify(result)).not.toContain('SENTINEL_SECRET'); expect(JSON.stringify(result)).not.toContain('vault-id'); - const insertCall = client.calls[2]; + const [, , insertCall] = client.calls; expect(insertCall?.sql).toContain( 'array(select jsonb_array_elements_text($7::jsonb))', ); @@ -105,7 +105,7 @@ describe('createSupabaseVaultAdapter', () => { expect(client.calls.some((call) => call.sql.includes('decrypted_secrets'))).toBe(false); expect(JSON.stringify(result)).not.toContain('ROTATED_SECRET'); - const updateCall = client.calls[2]; + const [, , updateCall] = client.calls; expect(updateCall?.sql).toContain('select jsonb_array_elements_text($4::jsonb)'); expect(updateCall?.parameters[3]).toBe('["clientSecret"]'); expect(Array.isArray(updateCall?.parameters[3])).toBe(false); From 7d6f91bf3924a53797241da245fa4a2385f6a5b5 Mon Sep 17 00:00:00 2001 From: artiphishle Date: Tue, 4 Aug 2026 23:54:58 +0200 Subject: [PATCH 5/5] docs: update --- README.md | 2 +- docs/badges/npm.svg | 6 +++--- docs/index.html | 27 ++++++++++++++++----------- docs/paradox.json | 37 ++++++++++++++++++++++++++----------- src/adapter.test.ts | 4 +--- 5 files changed, 47 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 7f41b3b..41c4843 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ # SUPABASE-VAULT -![license: MIT](././docs/badges/license.svg) ![npm: v0.2.0](././docs/badges/npm.svg) ![runtime: bun](././docs/badges/runtime.svg) ![typescript: strict](././docs/badges/typescript.svg) ![eslint: checked](././docs/badges/eslint.svg) ![prettier: checked](././docs/badges/prettier.svg) ![build: checked](././docs/badges/build.svg) ![tests: checked](././docs/badges/tests.svg) ![docs: paradox](././docs/badges/docs.svg) +![license: MIT](././docs/badges/license.svg) ![npm: v0.2.2](././docs/badges/npm.svg) ![runtime: bun](././docs/badges/runtime.svg) ![typescript: strict](././docs/badges/typescript.svg) ![eslint: checked](././docs/badges/eslint.svg) ![prettier: checked](././docs/badges/prettier.svg) ![build: checked](././docs/badges/build.svg) ![tests: checked](././docs/badges/tests.svg) ![docs: paradox](././docs/badges/docs.svg) Server-only Supabase Vault secret-store adapter for Ankhorage. diff --git a/docs/badges/npm.svg b/docs/badges/npm.svg index c6c55f1..327b1fa 100644 --- a/docs/badges/npm.svg +++ b/docs/badges/npm.svg @@ -1,7 +1,7 @@ - -npm: v0.2.0 + +npm: v0.2.2 npm -v0.2.0 +v0.2.2 diff --git a/docs/index.html b/docs/index.html index 015ac4b..6158424 100644 --- a/docs/index.html +++ b/docs/index.html @@ -198,7 +198,7 @@

SUPABASE-VAULT

  • @@ -704,52 +704,57 @@

    createSupabaseVaultAdapter

    normalizeLookup

    -

    src/adapter.ts:247:1

    +

    src/adapter.ts:253:1

    No description available.

    normalizeWrite

    -

    src/adapter.ts:255:1

    +

    src/adapter.ts:261:1

    No description available.

    selectMetadata

    -

    src/adapter.ts:263:1

    +

    src/adapter.ts:269:1

    No description available.

    selectInternalMetadata

    -

    src/adapter.ts:278:1

    +

    src/adapter.ts:284:1

    No description available.

    toMetadata

    -

    src/adapter.ts:293:1

    +

    src/adapter.ts:299:1

    +

    No description available.

    +
    +
    +

    serializeConfiguredFields

    +

    src/adapter.ts:311:1

    No description available.

    buildInternalName

    -

    src/adapter.ts:305:1

    +

    src/adapter.ts:315:1

    No description available.

    parsePayload

    -

    src/adapter.ts:309:1

    +

    src/adapter.ts:319:1

    No description available.

    notFound

    -

    src/adapter.ts:321:1

    +

    src/adapter.ts:331:1

    No description available.

    conflict

    -

    src/adapter.ts:328:1

    +

    src/adapter.ts:338:1

    No description available.

    providerFailure

    -

    src/adapter.ts:335:1

    +

    src/adapter.ts:345:1

    No description available.

    diff --git a/docs/paradox.json b/docs/paradox.json index cad24e0..17dbf74 100644 --- a/docs/paradox.json +++ b/docs/paradox.json @@ -12,7 +12,7 @@ { "id": "npm", "label": "npm", - "value": "v0.2.0", + "value": "v0.2.2", "color": "cb3837" }, { @@ -361,7 +361,7 @@ "description": null, "sourceLocation": { "filePath": "src/adapter.ts", - "line": 247, + "line": 253, "column": 1 } }, @@ -370,7 +370,7 @@ "description": null, "sourceLocation": { "filePath": "src/adapter.ts", - "line": 255, + "line": 261, "column": 1 } }, @@ -379,7 +379,7 @@ "description": null, "sourceLocation": { "filePath": "src/adapter.ts", - "line": 263, + "line": 269, "column": 1 } }, @@ -388,7 +388,7 @@ "description": null, "sourceLocation": { "filePath": "src/adapter.ts", - "line": 278, + "line": 284, "column": 1 } }, @@ -397,7 +397,16 @@ "description": null, "sourceLocation": { "filePath": "src/adapter.ts", - "line": 293, + "line": 299, + "column": 1 + } + }, + { + "name": "serializeConfiguredFields", + "description": null, + "sourceLocation": { + "filePath": "src/adapter.ts", + "line": 311, "column": 1 } }, @@ -406,7 +415,7 @@ "description": null, "sourceLocation": { "filePath": "src/adapter.ts", - "line": 305, + "line": 315, "column": 1 } }, @@ -415,7 +424,7 @@ "description": null, "sourceLocation": { "filePath": "src/adapter.ts", - "line": 309, + "line": 319, "column": 1 } }, @@ -424,7 +433,7 @@ "description": null, "sourceLocation": { "filePath": "src/adapter.ts", - "line": 321, + "line": 331, "column": 1 } }, @@ -433,7 +442,7 @@ "description": null, "sourceLocation": { "filePath": "src/adapter.ts", - "line": 328, + "line": 338, "column": 1 } }, @@ -442,7 +451,7 @@ "description": null, "sourceLocation": { "filePath": "src/adapter.ts", - "line": 335, + "line": 345, "column": 1 } } @@ -576,6 +585,12 @@ "callExpression": "providerFailure", "sourcePath": "src/adapter.ts" }, + { + "fromSymbol": "src/adapter.ts", + "toSymbol": "serializeConfiguredFields", + "callExpression": "serializeConfiguredFields", + "sourcePath": "src/adapter.ts" + }, { "fromSymbol": "src/adapter.ts", "toSymbol": "toMetadata", diff --git a/src/adapter.test.ts b/src/adapter.test.ts index b91ae79..95b7a3b 100644 --- a/src/adapter.test.ts +++ b/src/adapter.test.ts @@ -69,9 +69,7 @@ describe('createSupabaseVaultAdapter', () => { expect(JSON.stringify(result)).not.toContain('vault-id'); const [, , insertCall] = client.calls; - expect(insertCall?.sql).toContain( - 'array(select jsonb_array_elements_text($7::jsonb))', - ); + expect(insertCall?.sql).toContain('array(select jsonb_array_elements_text($7::jsonb))'); expect(insertCall?.parameters[6]).toBe('["clientId","clientSecret"]'); expect(Array.isArray(insertCall?.parameters[6])).toBe(false); });