@checkstack/script-packages-store-postgres 0.2.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 +50 -0
- package/drizzle/0000_thankful_doorman.sql +6 -0
- package/drizzle/meta/0000_snapshot.json +57 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +7 -0
- package/package.json +30 -0
- package/src/codec.test.ts +32 -0
- package/src/codec.ts +24 -0
- package/src/index.ts +38 -0
- package/src/plugin-metadata.ts +5 -0
- package/src/schema.ts +20 -0
- package/src/store.ts +65 -0
- package/tsconfig.json +20 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# @checkstack/script-packages-store-postgres
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 270ef29: Add the two built-in blob-store plugins for script packages.
|
|
8
|
+
|
|
9
|
+
- `script-packages-store-postgres` (default, zero extra infra): persists
|
|
10
|
+
content-addressed blobs in Postgres as base64 `text` (following the
|
|
11
|
+
existing repo convention for binary columns), registered as the
|
|
12
|
+
`postgres` backend via `blobStoreExtensionPoint`.
|
|
13
|
+
- `script-packages-store-s3` (preferred when configured): S3-compatible
|
|
14
|
+
store backed by Bun's native `S3Client` (no AWS SDK). Config from env
|
|
15
|
+
(`endpoint`, `bucket`, `region`, `accessKeyId`, `secretAccessKey`,
|
|
16
|
+
`forcePathStyle`); credentials never touch the DB. Registers the `s3`
|
|
17
|
+
backend only when configured, and reports a partial-config error
|
|
18
|
+
instead of silently falling back.
|
|
19
|
+
|
|
20
|
+
### Patch Changes
|
|
21
|
+
|
|
22
|
+
- b995afb: Harden the script-package blob byte boundary against a raw `ArrayBuffer`.
|
|
23
|
+
|
|
24
|
+
Blob bytes can reach the content-hash and storage codecs as a raw `ArrayBuffer` (e.g. from an S3/HTTP transport's `arrayBuffer()`), and Node/Bun's `crypto.Hash.update()` rejects a bare `ArrayBuffer` ("The 'data' argument must be of type string or an instance of Buffer, TypedArray, or DataView. Received an instance of ArrayBuffer"), which would fail a real-package install with `status=error`. `blobSha256` / `verifyBlobSha256` (script-packages-backend) and `encodeBlob` (script-packages-store-postgres) now normalize `ArrayBuffer` to a `Uint8Array` view at the boundary before hashing/encoding. A view over the same bytes hashes and encodes identically, so existing content hashes and stored blobs are unchanged. Adds regression tests feeding an `ArrayBuffer` through both the hash and the Postgres codec.
|
|
25
|
+
|
|
26
|
+
- Updated dependencies [270ef29]
|
|
27
|
+
- Updated dependencies [270ef29]
|
|
28
|
+
- Updated dependencies [270ef29]
|
|
29
|
+
- Updated dependencies [b995afb]
|
|
30
|
+
- Updated dependencies [270ef29]
|
|
31
|
+
- Updated dependencies [270ef29]
|
|
32
|
+
- Updated dependencies [270ef29]
|
|
33
|
+
- Updated dependencies [270ef29]
|
|
34
|
+
- Updated dependencies [b995afb]
|
|
35
|
+
- Updated dependencies [270ef29]
|
|
36
|
+
- Updated dependencies [b995afb]
|
|
37
|
+
- Updated dependencies [270ef29]
|
|
38
|
+
- Updated dependencies [270ef29]
|
|
39
|
+
- Updated dependencies [270ef29]
|
|
40
|
+
- Updated dependencies [b995afb]
|
|
41
|
+
- Updated dependencies [270ef29]
|
|
42
|
+
- Updated dependencies [b995afb]
|
|
43
|
+
- Updated dependencies [270ef29]
|
|
44
|
+
- Updated dependencies [270ef29]
|
|
45
|
+
- Updated dependencies [270ef29]
|
|
46
|
+
- Updated dependencies [270ef29]
|
|
47
|
+
- Updated dependencies [270ef29]
|
|
48
|
+
- @checkstack/backend-api@0.19.0
|
|
49
|
+
- @checkstack/script-packages-backend@0.2.0
|
|
50
|
+
- @checkstack/script-packages-common@0.2.0
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "f78fe1fa-2d5c-410f-859b-5d200702f378",
|
|
3
|
+
"prevId": "00000000-0000-0000-0000-000000000000",
|
|
4
|
+
"version": "7",
|
|
5
|
+
"dialect": "postgresql",
|
|
6
|
+
"tables": {
|
|
7
|
+
"public.script_package_blob_data": {
|
|
8
|
+
"name": "script_package_blob_data",
|
|
9
|
+
"schema": "",
|
|
10
|
+
"columns": {
|
|
11
|
+
"integrity": {
|
|
12
|
+
"name": "integrity",
|
|
13
|
+
"type": "text",
|
|
14
|
+
"primaryKey": true,
|
|
15
|
+
"notNull": true
|
|
16
|
+
},
|
|
17
|
+
"data": {
|
|
18
|
+
"name": "data",
|
|
19
|
+
"type": "text",
|
|
20
|
+
"primaryKey": false,
|
|
21
|
+
"notNull": true
|
|
22
|
+
},
|
|
23
|
+
"size_bytes": {
|
|
24
|
+
"name": "size_bytes",
|
|
25
|
+
"type": "bigint",
|
|
26
|
+
"primaryKey": false,
|
|
27
|
+
"notNull": true
|
|
28
|
+
},
|
|
29
|
+
"created_at": {
|
|
30
|
+
"name": "created_at",
|
|
31
|
+
"type": "timestamp",
|
|
32
|
+
"primaryKey": false,
|
|
33
|
+
"notNull": true,
|
|
34
|
+
"default": "now()"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"indexes": {},
|
|
38
|
+
"foreignKeys": {},
|
|
39
|
+
"compositePrimaryKeys": {},
|
|
40
|
+
"uniqueConstraints": {},
|
|
41
|
+
"policies": {},
|
|
42
|
+
"checkConstraints": {},
|
|
43
|
+
"isRLSEnabled": false
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"enums": {},
|
|
47
|
+
"schemas": {},
|
|
48
|
+
"sequences": {},
|
|
49
|
+
"roles": {},
|
|
50
|
+
"policies": {},
|
|
51
|
+
"views": {},
|
|
52
|
+
"_meta": {
|
|
53
|
+
"columns": {},
|
|
54
|
+
"schemas": {},
|
|
55
|
+
"tables": {}
|
|
56
|
+
}
|
|
57
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@checkstack/script-packages-store-postgres",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"license": "Elastic-2.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"checkstack": {
|
|
8
|
+
"type": "backend"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"typecheck": "tsgo -b",
|
|
12
|
+
"lint": "bun run lint:code",
|
|
13
|
+
"lint:code": "eslint . --max-warnings 0",
|
|
14
|
+
"test": "bun test"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@checkstack/backend-api": "0.18.0",
|
|
18
|
+
"@checkstack/common": "0.12.0",
|
|
19
|
+
"@checkstack/script-packages-backend": "0.1.0",
|
|
20
|
+
"@checkstack/script-packages-common": "0.1.0",
|
|
21
|
+
"drizzle-orm": "^0.45.0",
|
|
22
|
+
"zod": "^4.0.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@checkstack/scripts": "0.3.4",
|
|
26
|
+
"@checkstack/tsconfig": "0.0.7",
|
|
27
|
+
"drizzle-kit": "^0.31.10",
|
|
28
|
+
"typescript": "^5.7.2"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { encodeBlob, decodeBlob } from "./codec";
|
|
3
|
+
|
|
4
|
+
describe("blob codec", () => {
|
|
5
|
+
test("round-trips arbitrary bytes", () => {
|
|
6
|
+
const bytes = new Uint8Array([0, 1, 2, 250, 255, 128, 64]);
|
|
7
|
+
expect([...decodeBlob(encodeBlob(bytes))]).toEqual([...bytes]);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("round-trips an empty buffer", () => {
|
|
11
|
+
expect(decodeBlob(encodeBlob(new Uint8Array())).length).toBe(0);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("round-trips a larger random payload", () => {
|
|
15
|
+
const bytes = new Uint8Array(4096);
|
|
16
|
+
for (let i = 0; i < bytes.length; i++) bytes[i] = (i * 37) % 256;
|
|
17
|
+
const decoded = decodeBlob(encodeBlob(bytes));
|
|
18
|
+
expect(decoded.length).toBe(bytes.length);
|
|
19
|
+
expect([...decoded]).toEqual([...bytes]);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Regression: a store `put` may receive a raw ArrayBuffer from an S3/HTTP
|
|
23
|
+
// transport. `encodeBlob` must normalize it (not wrap the whole buffer) so
|
|
24
|
+
// the round-trip is byte-identical to the equivalent Uint8Array.
|
|
25
|
+
test("encodes a raw ArrayBuffer identically to its Uint8Array view", () => {
|
|
26
|
+
const u8 = new Uint8Array([0, 1, 2, 250, 255, 128, 64]);
|
|
27
|
+
const ab = u8.buffer.slice(0);
|
|
28
|
+
expect(ab).toBeInstanceOf(ArrayBuffer);
|
|
29
|
+
expect(encodeBlob(ab)).toBe(encodeBlob(u8));
|
|
30
|
+
expect([...decodeBlob(encodeBlob(ab))]).toEqual([...u8]);
|
|
31
|
+
});
|
|
32
|
+
});
|
package/src/codec.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base64 codec for blob bytes stored in the `text` column.
|
|
3
|
+
*
|
|
4
|
+
* Kept as a separate, pure module so the round-trip is unit-testable
|
|
5
|
+
* without a database.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Bytes as they arrive at the encode boundary from any blob source. */
|
|
9
|
+
export type BlobBytes = Uint8Array | ArrayBuffer;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Base64-encode blob bytes for the `text` column. Accepts a raw `ArrayBuffer`
|
|
13
|
+
* (e.g. from an S3/HTTP transport) as well as a `Uint8Array`: `Buffer.from`
|
|
14
|
+
* wraps an `ArrayBuffer` over the whole buffer, so we normalize to a view
|
|
15
|
+
* first to encode exactly the intended bytes.
|
|
16
|
+
*/
|
|
17
|
+
export function encodeBlob(bytes: BlobBytes): string {
|
|
18
|
+
const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
19
|
+
return Buffer.from(view).toString("base64");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function decodeBlob(data: string): Uint8Array {
|
|
23
|
+
return new Uint8Array(Buffer.from(data, "base64"));
|
|
24
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createBackendPlugin, coreServices } from "@checkstack/backend-api";
|
|
2
|
+
import { blobStoreExtensionPoint } from "@checkstack/script-packages-backend";
|
|
3
|
+
import { pluginMetadata } from "./plugin-metadata";
|
|
4
|
+
import { createPostgresBlobStore, POSTGRES_BLOB_STORE_ID } from "./store";
|
|
5
|
+
import * as schema from "./schema";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Default blob-store plugin: persists script-package blobs in Postgres
|
|
9
|
+
* (base64 `text`). Always available; selected when no S3 is configured.
|
|
10
|
+
*
|
|
11
|
+
* The store is built in `init()` (where `database` is injected) and
|
|
12
|
+
* registered with the host's `blobStoreExtensionPoint`. The host runs its
|
|
13
|
+
* `register()` first (dep ordering), so the extension point exists by the
|
|
14
|
+
* time this init runs.
|
|
15
|
+
*/
|
|
16
|
+
export default createBackendPlugin({
|
|
17
|
+
metadata: pluginMetadata,
|
|
18
|
+
|
|
19
|
+
register(env) {
|
|
20
|
+
env.registerInit({
|
|
21
|
+
schema,
|
|
22
|
+
deps: {
|
|
23
|
+
logger: coreServices.logger,
|
|
24
|
+
},
|
|
25
|
+
init: async ({ logger, database }) => {
|
|
26
|
+
const store = createPostgresBlobStore(database);
|
|
27
|
+
env
|
|
28
|
+
.getExtensionPoint(blobStoreExtensionPoint)
|
|
29
|
+
.registerBlobStore(store, pluginMetadata);
|
|
30
|
+
logger.debug(
|
|
31
|
+
`📦 Registered "${POSTGRES_BLOB_STORE_ID}" script-package blob store.`,
|
|
32
|
+
);
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export { createPostgresBlobStore, POSTGRES_BLOB_STORE_ID } from "./store";
|
package/src/schema.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { pgTable, text, bigint, timestamp } from "drizzle-orm/pg-core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Postgres blob store schema.
|
|
5
|
+
*
|
|
6
|
+
* One row per content-addressed blob, keyed by integrity. Compressed blob
|
|
7
|
+
* bytes are stored base64-encoded in a `text` column - following the
|
|
8
|
+
* existing repo convention (`core/backend/src/schema.ts` does the same for
|
|
9
|
+
* plugin tarballs) because Drizzle's `bytea` handling is awkward and base64
|
|
10
|
+
* `text` is portable. Blobs are bounded by the size guardrail and the
|
|
11
|
+
* lightweight-pure-JS regime, so the ~33% base64 overhead is acceptable for
|
|
12
|
+
* the zero-extra-infra default backend.
|
|
13
|
+
*/
|
|
14
|
+
export const scriptPackageBlobData = pgTable("script_package_blob_data", {
|
|
15
|
+
integrity: text("integrity").primaryKey(),
|
|
16
|
+
/** base64-encoded compressed blob bytes. */
|
|
17
|
+
data: text("data").notNull(),
|
|
18
|
+
sizeBytes: bigint("size_bytes", { mode: "number" }).notNull(),
|
|
19
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
20
|
+
});
|
package/src/store.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { eq } from "drizzle-orm";
|
|
2
|
+
import type { SafeDatabase } from "@checkstack/backend-api";
|
|
3
|
+
import type { BlobStore } from "@checkstack/script-packages-backend";
|
|
4
|
+
import { scriptPackageBlobData } from "./schema";
|
|
5
|
+
import { encodeBlob, decodeBlob } from "./codec";
|
|
6
|
+
|
|
7
|
+
export const POSTGRES_BLOB_STORE_ID = "postgres";
|
|
8
|
+
|
|
9
|
+
type Schema = { scriptPackageBlobData: typeof scriptPackageBlobData };
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Postgres-backed {@link BlobStore} - the default, zero-extra-infra
|
|
13
|
+
* backend. Content-addressed: `put` of an already-present integrity is a
|
|
14
|
+
* no-op (identical bytes by definition), so we use `onConflictDoNothing`.
|
|
15
|
+
*/
|
|
16
|
+
export function createPostgresBlobStore(
|
|
17
|
+
db: SafeDatabase<Schema>,
|
|
18
|
+
): BlobStore {
|
|
19
|
+
return {
|
|
20
|
+
id: POSTGRES_BLOB_STORE_ID,
|
|
21
|
+
|
|
22
|
+
async put({ integrity, bytes }) {
|
|
23
|
+
await db
|
|
24
|
+
.insert(scriptPackageBlobData)
|
|
25
|
+
.values({
|
|
26
|
+
integrity,
|
|
27
|
+
data: encodeBlob(bytes),
|
|
28
|
+
sizeBytes: bytes.byteLength,
|
|
29
|
+
})
|
|
30
|
+
.onConflictDoNothing({ target: scriptPackageBlobData.integrity });
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
async get({ integrity }) {
|
|
34
|
+
const rows = await db
|
|
35
|
+
.select({ data: scriptPackageBlobData.data })
|
|
36
|
+
.from(scriptPackageBlobData)
|
|
37
|
+
.where(eq(scriptPackageBlobData.integrity, integrity))
|
|
38
|
+
.limit(1);
|
|
39
|
+
const row = rows[0];
|
|
40
|
+
return row ? decodeBlob(row.data) : undefined;
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
async has({ integrity }) {
|
|
44
|
+
const rows = await db
|
|
45
|
+
.select({ integrity: scriptPackageBlobData.integrity })
|
|
46
|
+
.from(scriptPackageBlobData)
|
|
47
|
+
.where(eq(scriptPackageBlobData.integrity, integrity))
|
|
48
|
+
.limit(1);
|
|
49
|
+
return rows.length > 0;
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
async delete({ integrity }) {
|
|
53
|
+
await db
|
|
54
|
+
.delete(scriptPackageBlobData)
|
|
55
|
+
.where(eq(scriptPackageBlobData.integrity, integrity));
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
async list() {
|
|
59
|
+
const rows = await db
|
|
60
|
+
.select({ integrity: scriptPackageBlobData.integrity })
|
|
61
|
+
.from(scriptPackageBlobData);
|
|
62
|
+
return rows.map((r) => r.integrity);
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "@checkstack/tsconfig/backend.json",
|
|
3
|
+
"include": [
|
|
4
|
+
"src"
|
|
5
|
+
],
|
|
6
|
+
"references": [
|
|
7
|
+
{
|
|
8
|
+
"path": "../backend-api"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"path": "../common"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"path": "../script-packages-backend"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"path": "../script-packages-common"
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|