@absurd-sqlite/sdk 0.2.1-alpha.0 → 0.2.1-alpha.2
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/dist/cjs/extension-downloader.js +189 -0
- package/dist/cjs/index.js +4 -1
- package/dist/extension-downloader.d.ts +76 -0
- package/dist/extension-downloader.d.ts.map +1 -0
- package/dist/extension-downloader.js +186 -0
- package/dist/extension-downloader.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/extension-downloader.ts +291 -0
- package/src/index.ts +5 -0
- package/test/extension-downloader.test.ts +203 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.downloadExtension = downloadExtension;
|
|
4
|
+
exports.resolveExtensionPath = resolveExtensionPath;
|
|
5
|
+
const node_fs_1 = require("node:fs");
|
|
6
|
+
const node_path_1 = require("node:path");
|
|
7
|
+
const node_os_1 = require("node:os");
|
|
8
|
+
const node_crypto_1 = require("node:crypto");
|
|
9
|
+
function getPlatformInfo() {
|
|
10
|
+
const platform = process.platform;
|
|
11
|
+
const arch = process.arch;
|
|
12
|
+
let os;
|
|
13
|
+
let ext;
|
|
14
|
+
switch (platform) {
|
|
15
|
+
case "darwin":
|
|
16
|
+
os = "macOS";
|
|
17
|
+
ext = "dylib";
|
|
18
|
+
break;
|
|
19
|
+
case "linux":
|
|
20
|
+
os = "Linux";
|
|
21
|
+
ext = "so";
|
|
22
|
+
break;
|
|
23
|
+
case "win32":
|
|
24
|
+
os = "Windows";
|
|
25
|
+
ext = "dll";
|
|
26
|
+
break;
|
|
27
|
+
default:
|
|
28
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
29
|
+
}
|
|
30
|
+
let archStr;
|
|
31
|
+
switch (arch) {
|
|
32
|
+
case "x64":
|
|
33
|
+
archStr = "X64";
|
|
34
|
+
break;
|
|
35
|
+
case "arm64":
|
|
36
|
+
archStr = "ARM64";
|
|
37
|
+
break;
|
|
38
|
+
default:
|
|
39
|
+
throw new Error(`Unsupported architecture: ${arch}`);
|
|
40
|
+
}
|
|
41
|
+
return { os, arch: archStr, ext };
|
|
42
|
+
}
|
|
43
|
+
function getDefaultCacheDir() {
|
|
44
|
+
return (0, node_path_1.join)((0, node_os_1.homedir)(), ".cache", "absurd-sqlite", "extensions");
|
|
45
|
+
}
|
|
46
|
+
function getAssetName(version, platform) {
|
|
47
|
+
// Format: absurd-absurd-sqlite-extension-vX.Y.Z-{OS}-{ARCH}.{ext}
|
|
48
|
+
return `absurd-absurd-sqlite-extension-${version}-${platform.os}-${platform.arch}.${platform.ext}`;
|
|
49
|
+
}
|
|
50
|
+
function getTag(version) {
|
|
51
|
+
return `absurd-sqlite-extension/${version}`;
|
|
52
|
+
}
|
|
53
|
+
async function fetchLatestVersion(owner, repo) {
|
|
54
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/releases`;
|
|
55
|
+
const response = await fetch(url);
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
throw new Error(`Failed to fetch releases: ${response.status} ${response.statusText}`);
|
|
58
|
+
}
|
|
59
|
+
const releases = (await response.json());
|
|
60
|
+
// Find the latest non-draft extension release
|
|
61
|
+
for (const release of releases) {
|
|
62
|
+
if (!release.draft &&
|
|
63
|
+
release.tag_name.startsWith("absurd-sqlite-extension/")) {
|
|
64
|
+
// Extract version from tag (e.g., "absurd-sqlite-extension/v0.1.0-alpha.3" -> "v0.1.0-alpha.3")
|
|
65
|
+
return release.tag_name.replace("absurd-sqlite-extension/", "");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
throw new Error("No extension releases found");
|
|
69
|
+
}
|
|
70
|
+
async function downloadAsset(owner, repo, tag, assetName, destPath, expectedChecksum) {
|
|
71
|
+
const url = `https://github.com/${owner}/${repo}/releases/download/${tag}/${assetName}`;
|
|
72
|
+
const response = await fetch(url);
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
throw new Error(`Failed to download extension: ${response.status} ${response.statusText} from ${url}`);
|
|
75
|
+
}
|
|
76
|
+
const buffer = await response.arrayBuffer();
|
|
77
|
+
(0, node_fs_1.writeFileSync)(destPath, Buffer.from(buffer));
|
|
78
|
+
// Verify checksum if provided
|
|
79
|
+
if (expectedChecksum) {
|
|
80
|
+
const actualChecksum = calculateChecksum(destPath);
|
|
81
|
+
if (actualChecksum !== expectedChecksum.toLowerCase()) {
|
|
82
|
+
throw new Error(`Checksum verification failed. Expected: ${expectedChecksum.toLowerCase()}, Got: ${actualChecksum}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Make the extension executable on Unix-like systems
|
|
86
|
+
if (process.platform !== "win32") {
|
|
87
|
+
(0, node_fs_1.chmodSync)(destPath, 0o755);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function calculateChecksum(filePath) {
|
|
91
|
+
const fileBuffer = (0, node_fs_1.readFileSync)(filePath);
|
|
92
|
+
const hash = (0, node_crypto_1.createHash)("sha256");
|
|
93
|
+
hash.update(fileBuffer);
|
|
94
|
+
return hash.digest("hex");
|
|
95
|
+
}
|
|
96
|
+
function getCachedPath(cacheDir, version, platform) {
|
|
97
|
+
// Use a consistent name that SQLite expects: libabsurd.{ext}
|
|
98
|
+
const ext = platform.ext;
|
|
99
|
+
return (0, node_path_1.join)(cacheDir, version, `libabsurd.${ext}`);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Downloads the absurd-sqlite extension from GitHub releases.
|
|
103
|
+
* Returns the path to the downloaded extension file.
|
|
104
|
+
*
|
|
105
|
+
* @param options - Download options
|
|
106
|
+
* @returns Path to the extension file
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* ```typescript
|
|
110
|
+
* import { downloadExtension } from "@absurd-sqlite/sdk";
|
|
111
|
+
*
|
|
112
|
+
* // Download latest version
|
|
113
|
+
* const extensionPath = await downloadExtension();
|
|
114
|
+
*
|
|
115
|
+
* // Download specific version
|
|
116
|
+
* const extensionPath = await downloadExtension({ version: "v0.1.0-alpha.3" });
|
|
117
|
+
*
|
|
118
|
+
* // Download with checksum verification
|
|
119
|
+
* const extensionPath = await downloadExtension({
|
|
120
|
+
* version: "v0.1.0-alpha.3",
|
|
121
|
+
* expectedChecksum: "abc123..."
|
|
122
|
+
* });
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
async function downloadExtension(options = {}) {
|
|
126
|
+
const owner = options.owner ?? "b4fun";
|
|
127
|
+
const repo = options.repo ?? "absurd-sqlite";
|
|
128
|
+
const cacheDir = options.cacheDir ?? getDefaultCacheDir();
|
|
129
|
+
const force = options.force ?? false;
|
|
130
|
+
// Resolve version
|
|
131
|
+
let version = options.version ?? "latest";
|
|
132
|
+
if (version === "latest") {
|
|
133
|
+
version = await fetchLatestVersion(owner, repo);
|
|
134
|
+
}
|
|
135
|
+
// Get platform info
|
|
136
|
+
const platform = getPlatformInfo();
|
|
137
|
+
const assetName = getAssetName(version, platform);
|
|
138
|
+
const tag = getTag(version);
|
|
139
|
+
// Check cache
|
|
140
|
+
const cachedPath = getCachedPath(cacheDir, version, platform);
|
|
141
|
+
if (!force && (0, node_fs_1.existsSync)(cachedPath)) {
|
|
142
|
+
// If checksum is provided, verify cached file
|
|
143
|
+
if (options.expectedChecksum) {
|
|
144
|
+
const actualChecksum = calculateChecksum(cachedPath);
|
|
145
|
+
if (actualChecksum !== options.expectedChecksum.toLowerCase()) {
|
|
146
|
+
throw new Error(`Cached file checksum verification failed. Expected: ${options.expectedChecksum.toLowerCase()}, Got: ${actualChecksum}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return cachedPath;
|
|
150
|
+
}
|
|
151
|
+
// Ensure cache directory exists
|
|
152
|
+
const versionDir = (0, node_path_1.join)(cacheDir, version);
|
|
153
|
+
(0, node_fs_1.mkdirSync)(versionDir, { recursive: true });
|
|
154
|
+
// Download asset
|
|
155
|
+
await downloadAsset(owner, repo, tag, assetName, cachedPath, options.expectedChecksum);
|
|
156
|
+
return cachedPath;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Resolves the extension path, either from the provided path, environment variable,
|
|
160
|
+
* or by downloading from GitHub releases.
|
|
161
|
+
*
|
|
162
|
+
* @param extensionPath - Optional path to the extension file
|
|
163
|
+
* @param downloadOptions - Options for downloading if no path is provided
|
|
164
|
+
* @returns Path to the extension file
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* ```typescript
|
|
168
|
+
* import { resolveExtensionPath } from "@absurd-sqlite/sdk";
|
|
169
|
+
*
|
|
170
|
+
* // Use provided path
|
|
171
|
+
* const path1 = await resolveExtensionPath("/path/to/extension.so");
|
|
172
|
+
*
|
|
173
|
+
* // Use environment variable or download
|
|
174
|
+
* const path2 = await resolveExtensionPath();
|
|
175
|
+
* ```
|
|
176
|
+
*/
|
|
177
|
+
async function resolveExtensionPath(extensionPath, downloadOptions) {
|
|
178
|
+
// If path is provided, use it
|
|
179
|
+
if (extensionPath) {
|
|
180
|
+
return extensionPath;
|
|
181
|
+
}
|
|
182
|
+
// Try environment variable
|
|
183
|
+
const envPath = process.env.ABSURD_SQLITE_EXTENSION_PATH;
|
|
184
|
+
if (envPath) {
|
|
185
|
+
return envPath;
|
|
186
|
+
}
|
|
187
|
+
// Download from GitHub releases
|
|
188
|
+
return downloadExtension(downloadOptions);
|
|
189
|
+
}
|
package/dist/cjs/index.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Absurd = void 0;
|
|
3
|
+
exports.Absurd = exports.resolveExtensionPath = exports.downloadExtension = void 0;
|
|
4
4
|
const absurd_sdk_1 = require("absurd-sdk");
|
|
5
5
|
const sqlite_1 = require("./sqlite");
|
|
6
|
+
var extension_downloader_1 = require("./extension-downloader");
|
|
7
|
+
Object.defineProperty(exports, "downloadExtension", { enumerable: true, get: function () { return extension_downloader_1.downloadExtension; } });
|
|
8
|
+
Object.defineProperty(exports, "resolveExtensionPath", { enumerable: true, get: function () { return extension_downloader_1.resolveExtensionPath; } });
|
|
6
9
|
class Absurd extends absurd_sdk_1.Absurd {
|
|
7
10
|
db;
|
|
8
11
|
constructor(db, extensionPath) {
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export interface DownloadExtensionOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Version of the extension to download. If not specified, uses "latest".
|
|
4
|
+
* Examples: "v0.1.0-alpha.3", "latest"
|
|
5
|
+
*/
|
|
6
|
+
version?: string;
|
|
7
|
+
/**
|
|
8
|
+
* GitHub repository owner. Defaults to "b4fun".
|
|
9
|
+
*/
|
|
10
|
+
owner?: string;
|
|
11
|
+
/**
|
|
12
|
+
* GitHub repository name. Defaults to "absurd-sqlite".
|
|
13
|
+
*/
|
|
14
|
+
repo?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Custom cache directory for storing downloaded extensions.
|
|
17
|
+
* If not specified, uses a default cache directory in user's home.
|
|
18
|
+
*/
|
|
19
|
+
cacheDir?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Force re-download even if cached version exists.
|
|
22
|
+
*/
|
|
23
|
+
force?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Expected SHA256 checksum of the downloaded file for validation.
|
|
26
|
+
* If provided, the downloaded file's checksum will be verified against this value.
|
|
27
|
+
* If the checksum doesn't match, an error will be thrown.
|
|
28
|
+
*/
|
|
29
|
+
expectedChecksum?: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Downloads the absurd-sqlite extension from GitHub releases.
|
|
33
|
+
* Returns the path to the downloaded extension file.
|
|
34
|
+
*
|
|
35
|
+
* @param options - Download options
|
|
36
|
+
* @returns Path to the extension file
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```typescript
|
|
40
|
+
* import { downloadExtension } from "@absurd-sqlite/sdk";
|
|
41
|
+
*
|
|
42
|
+
* // Download latest version
|
|
43
|
+
* const extensionPath = await downloadExtension();
|
|
44
|
+
*
|
|
45
|
+
* // Download specific version
|
|
46
|
+
* const extensionPath = await downloadExtension({ version: "v0.1.0-alpha.3" });
|
|
47
|
+
*
|
|
48
|
+
* // Download with checksum verification
|
|
49
|
+
* const extensionPath = await downloadExtension({
|
|
50
|
+
* version: "v0.1.0-alpha.3",
|
|
51
|
+
* expectedChecksum: "abc123..."
|
|
52
|
+
* });
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export declare function downloadExtension(options?: DownloadExtensionOptions): Promise<string>;
|
|
56
|
+
/**
|
|
57
|
+
* Resolves the extension path, either from the provided path, environment variable,
|
|
58
|
+
* or by downloading from GitHub releases.
|
|
59
|
+
*
|
|
60
|
+
* @param extensionPath - Optional path to the extension file
|
|
61
|
+
* @param downloadOptions - Options for downloading if no path is provided
|
|
62
|
+
* @returns Path to the extension file
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```typescript
|
|
66
|
+
* import { resolveExtensionPath } from "@absurd-sqlite/sdk";
|
|
67
|
+
*
|
|
68
|
+
* // Use provided path
|
|
69
|
+
* const path1 = await resolveExtensionPath("/path/to/extension.so");
|
|
70
|
+
*
|
|
71
|
+
* // Use environment variable or download
|
|
72
|
+
* const path2 = await resolveExtensionPath();
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export declare function resolveExtensionPath(extensionPath?: string, downloadOptions?: DownloadExtensionOptions): Promise<string>;
|
|
76
|
+
//# sourceMappingURL=extension-downloader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extension-downloader.d.ts","sourceRoot":"","sources":["../src/extension-downloader.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,wBAAwB;IACvC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAkJD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,GAAE,wBAA6B,GACrC,OAAO,CAAC,MAAM,CAAC,CAyCjB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,oBAAoB,CACxC,aAAa,CAAC,EAAE,MAAM,EACtB,eAAe,CAAC,EAAE,wBAAwB,GACzC,OAAO,CAAC,MAAM,CAAC,CAcjB"}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync, chmodSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
function getPlatformInfo() {
|
|
6
|
+
const platform = process.platform;
|
|
7
|
+
const arch = process.arch;
|
|
8
|
+
let os;
|
|
9
|
+
let ext;
|
|
10
|
+
switch (platform) {
|
|
11
|
+
case "darwin":
|
|
12
|
+
os = "macOS";
|
|
13
|
+
ext = "dylib";
|
|
14
|
+
break;
|
|
15
|
+
case "linux":
|
|
16
|
+
os = "Linux";
|
|
17
|
+
ext = "so";
|
|
18
|
+
break;
|
|
19
|
+
case "win32":
|
|
20
|
+
os = "Windows";
|
|
21
|
+
ext = "dll";
|
|
22
|
+
break;
|
|
23
|
+
default:
|
|
24
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
25
|
+
}
|
|
26
|
+
let archStr;
|
|
27
|
+
switch (arch) {
|
|
28
|
+
case "x64":
|
|
29
|
+
archStr = "X64";
|
|
30
|
+
break;
|
|
31
|
+
case "arm64":
|
|
32
|
+
archStr = "ARM64";
|
|
33
|
+
break;
|
|
34
|
+
default:
|
|
35
|
+
throw new Error(`Unsupported architecture: ${arch}`);
|
|
36
|
+
}
|
|
37
|
+
return { os, arch: archStr, ext };
|
|
38
|
+
}
|
|
39
|
+
function getDefaultCacheDir() {
|
|
40
|
+
return join(homedir(), ".cache", "absurd-sqlite", "extensions");
|
|
41
|
+
}
|
|
42
|
+
function getAssetName(version, platform) {
|
|
43
|
+
// Format: absurd-absurd-sqlite-extension-vX.Y.Z-{OS}-{ARCH}.{ext}
|
|
44
|
+
return `absurd-absurd-sqlite-extension-${version}-${platform.os}-${platform.arch}.${platform.ext}`;
|
|
45
|
+
}
|
|
46
|
+
function getTag(version) {
|
|
47
|
+
return `absurd-sqlite-extension/${version}`;
|
|
48
|
+
}
|
|
49
|
+
async function fetchLatestVersion(owner, repo) {
|
|
50
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/releases`;
|
|
51
|
+
const response = await fetch(url);
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
throw new Error(`Failed to fetch releases: ${response.status} ${response.statusText}`);
|
|
54
|
+
}
|
|
55
|
+
const releases = (await response.json());
|
|
56
|
+
// Find the latest non-draft extension release
|
|
57
|
+
for (const release of releases) {
|
|
58
|
+
if (!release.draft &&
|
|
59
|
+
release.tag_name.startsWith("absurd-sqlite-extension/")) {
|
|
60
|
+
// Extract version from tag (e.g., "absurd-sqlite-extension/v0.1.0-alpha.3" -> "v0.1.0-alpha.3")
|
|
61
|
+
return release.tag_name.replace("absurd-sqlite-extension/", "");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
throw new Error("No extension releases found");
|
|
65
|
+
}
|
|
66
|
+
async function downloadAsset(owner, repo, tag, assetName, destPath, expectedChecksum) {
|
|
67
|
+
const url = `https://github.com/${owner}/${repo}/releases/download/${tag}/${assetName}`;
|
|
68
|
+
const response = await fetch(url);
|
|
69
|
+
if (!response.ok) {
|
|
70
|
+
throw new Error(`Failed to download extension: ${response.status} ${response.statusText} from ${url}`);
|
|
71
|
+
}
|
|
72
|
+
const buffer = await response.arrayBuffer();
|
|
73
|
+
writeFileSync(destPath, Buffer.from(buffer));
|
|
74
|
+
// Verify checksum if provided
|
|
75
|
+
if (expectedChecksum) {
|
|
76
|
+
const actualChecksum = calculateChecksum(destPath);
|
|
77
|
+
if (actualChecksum !== expectedChecksum.toLowerCase()) {
|
|
78
|
+
throw new Error(`Checksum verification failed. Expected: ${expectedChecksum.toLowerCase()}, Got: ${actualChecksum}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Make the extension executable on Unix-like systems
|
|
82
|
+
if (process.platform !== "win32") {
|
|
83
|
+
chmodSync(destPath, 0o755);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function calculateChecksum(filePath) {
|
|
87
|
+
const fileBuffer = readFileSync(filePath);
|
|
88
|
+
const hash = createHash("sha256");
|
|
89
|
+
hash.update(fileBuffer);
|
|
90
|
+
return hash.digest("hex");
|
|
91
|
+
}
|
|
92
|
+
function getCachedPath(cacheDir, version, platform) {
|
|
93
|
+
// Use a consistent name that SQLite expects: libabsurd.{ext}
|
|
94
|
+
const ext = platform.ext;
|
|
95
|
+
return join(cacheDir, version, `libabsurd.${ext}`);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Downloads the absurd-sqlite extension from GitHub releases.
|
|
99
|
+
* Returns the path to the downloaded extension file.
|
|
100
|
+
*
|
|
101
|
+
* @param options - Download options
|
|
102
|
+
* @returns Path to the extension file
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```typescript
|
|
106
|
+
* import { downloadExtension } from "@absurd-sqlite/sdk";
|
|
107
|
+
*
|
|
108
|
+
* // Download latest version
|
|
109
|
+
* const extensionPath = await downloadExtension();
|
|
110
|
+
*
|
|
111
|
+
* // Download specific version
|
|
112
|
+
* const extensionPath = await downloadExtension({ version: "v0.1.0-alpha.3" });
|
|
113
|
+
*
|
|
114
|
+
* // Download with checksum verification
|
|
115
|
+
* const extensionPath = await downloadExtension({
|
|
116
|
+
* version: "v0.1.0-alpha.3",
|
|
117
|
+
* expectedChecksum: "abc123..."
|
|
118
|
+
* });
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
export async function downloadExtension(options = {}) {
|
|
122
|
+
const owner = options.owner ?? "b4fun";
|
|
123
|
+
const repo = options.repo ?? "absurd-sqlite";
|
|
124
|
+
const cacheDir = options.cacheDir ?? getDefaultCacheDir();
|
|
125
|
+
const force = options.force ?? false;
|
|
126
|
+
// Resolve version
|
|
127
|
+
let version = options.version ?? "latest";
|
|
128
|
+
if (version === "latest") {
|
|
129
|
+
version = await fetchLatestVersion(owner, repo);
|
|
130
|
+
}
|
|
131
|
+
// Get platform info
|
|
132
|
+
const platform = getPlatformInfo();
|
|
133
|
+
const assetName = getAssetName(version, platform);
|
|
134
|
+
const tag = getTag(version);
|
|
135
|
+
// Check cache
|
|
136
|
+
const cachedPath = getCachedPath(cacheDir, version, platform);
|
|
137
|
+
if (!force && existsSync(cachedPath)) {
|
|
138
|
+
// If checksum is provided, verify cached file
|
|
139
|
+
if (options.expectedChecksum) {
|
|
140
|
+
const actualChecksum = calculateChecksum(cachedPath);
|
|
141
|
+
if (actualChecksum !== options.expectedChecksum.toLowerCase()) {
|
|
142
|
+
throw new Error(`Cached file checksum verification failed. Expected: ${options.expectedChecksum.toLowerCase()}, Got: ${actualChecksum}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return cachedPath;
|
|
146
|
+
}
|
|
147
|
+
// Ensure cache directory exists
|
|
148
|
+
const versionDir = join(cacheDir, version);
|
|
149
|
+
mkdirSync(versionDir, { recursive: true });
|
|
150
|
+
// Download asset
|
|
151
|
+
await downloadAsset(owner, repo, tag, assetName, cachedPath, options.expectedChecksum);
|
|
152
|
+
return cachedPath;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Resolves the extension path, either from the provided path, environment variable,
|
|
156
|
+
* or by downloading from GitHub releases.
|
|
157
|
+
*
|
|
158
|
+
* @param extensionPath - Optional path to the extension file
|
|
159
|
+
* @param downloadOptions - Options for downloading if no path is provided
|
|
160
|
+
* @returns Path to the extension file
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* ```typescript
|
|
164
|
+
* import { resolveExtensionPath } from "@absurd-sqlite/sdk";
|
|
165
|
+
*
|
|
166
|
+
* // Use provided path
|
|
167
|
+
* const path1 = await resolveExtensionPath("/path/to/extension.so");
|
|
168
|
+
*
|
|
169
|
+
* // Use environment variable or download
|
|
170
|
+
* const path2 = await resolveExtensionPath();
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
export async function resolveExtensionPath(extensionPath, downloadOptions) {
|
|
174
|
+
// If path is provided, use it
|
|
175
|
+
if (extensionPath) {
|
|
176
|
+
return extensionPath;
|
|
177
|
+
}
|
|
178
|
+
// Try environment variable
|
|
179
|
+
const envPath = process.env.ABSURD_SQLITE_EXTENSION_PATH;
|
|
180
|
+
if (envPath) {
|
|
181
|
+
return envPath;
|
|
182
|
+
}
|
|
183
|
+
// Download from GitHub releases
|
|
184
|
+
return downloadExtension(downloadOptions);
|
|
185
|
+
}
|
|
186
|
+
//# sourceMappingURL=extension-downloader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extension-downloader.js","sourceRoot":"","sources":["../src/extension-downloader.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Absurd as AbsurdBase } from "absurd-sdk";
|
|
|
2
2
|
import type { AbsurdClient } from "./absurd-types";
|
|
3
3
|
import type { SQLiteDatabase } from "./sqlite-types";
|
|
4
4
|
export type { AbsurdClient, Queryable, Worker } from "./absurd-types";
|
|
5
|
+
export { downloadExtension, resolveExtensionPath, type DownloadExtensionOptions, } from "./extension-downloader";
|
|
5
6
|
export type { AbsurdHooks, AbsurdOptions, CancellationPolicy, ClaimedTask, JsonObject, JsonValue, RetryStrategy, SpawnOptions, SpawnResult, TaskContext, TaskHandler, TaskRegistrationOptions, WorkerOptions, } from "absurd-sdk";
|
|
6
7
|
export type { SQLiteBindParams, SQLiteBindValue, SQLiteColumnDefinition, SQLiteDatabase, SQLiteRestBindParams, SQLiteStatement, SQLiteVerboseLog, } from "./sqlite-types";
|
|
7
8
|
export declare class Absurd extends AbsurdBase implements AbsurdClient {
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,YAAY,CAAC;AAElD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAGrD,YAAY,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACtE,YAAY,EACV,WAAW,EACX,aAAa,EACb,kBAAkB,EAClB,WAAW,EACX,UAAU,EACV,SAAS,EACT,aAAa,EACb,YAAY,EACZ,WAAW,EACX,WAAW,EACX,WAAW,EACX,uBAAuB,EACvB,aAAa,GACd,MAAM,YAAY,CAAC;AACpB,YAAY,EACV,gBAAgB,EAChB,eAAe,EACf,sBAAsB,EACtB,cAAc,EACd,oBAAoB,EACpB,eAAe,EACf,gBAAgB,GACjB,MAAM,gBAAgB,CAAC;AAExB,qBAAa,MAAO,SAAQ,UAAW,YAAW,YAAY;IAC5D,OAAO,CAAC,EAAE,CAAiB;gBAEf,EAAE,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM;IAO/C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,YAAY,CAAC;AAElD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAGrD,YAAY,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACtE,OAAO,EACL,iBAAiB,EACjB,oBAAoB,EACpB,KAAK,wBAAwB,GAC9B,MAAM,wBAAwB,CAAC;AAChC,YAAY,EACV,WAAW,EACX,aAAa,EACb,kBAAkB,EAClB,WAAW,EACX,UAAU,EACV,SAAS,EACT,aAAa,EACb,YAAY,EACZ,WAAW,EACX,WAAW,EACX,WAAW,EACX,uBAAuB,EACvB,aAAa,GACd,MAAM,YAAY,CAAC;AACpB,YAAY,EACV,gBAAgB,EAChB,eAAe,EACf,sBAAsB,EACtB,cAAc,EACd,oBAAoB,EACpB,eAAe,EACf,gBAAgB,GACjB,MAAM,gBAAgB,CAAC;AAExB,qBAAa,MAAO,SAAQ,UAAW,YAAW,YAAY;IAC5D,OAAO,CAAC,EAAE,CAAiB;gBAEf,EAAE,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM;IAO/C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B"}
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,YAAY,CAAC;AAIlD,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,YAAY,CAAC;AAIlD,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAG5C,OAAO,EACL,iBAAiB,EACjB,oBAAoB,GAErB,MAAM,wBAAwB,CAAC;AA0BhC,MAAM,OAAO,MAAO,SAAQ,UAAU;IAC5B,EAAE,CAAiB;IAE3B,YAAY,EAAkB,EAAE,aAAqB;QACnD,EAAE,CAAC,aAAa,CAAC,aAAa,CAAC,CAAC;QAChC,MAAM,SAAS,GAAG,IAAI,gBAAgB,CAAC,EAAE,CAAC,CAAC;QAC3C,KAAK,CAAC,SAAS,CAAC,CAAC;QACjB,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC;IACf,CAAC;IAED,KAAK,CAAC,KAAK;QACT,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC;IAClB,CAAC;CACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@absurd-sqlite/sdk",
|
|
3
|
-
"version": "0.2.1-alpha.
|
|
3
|
+
"version": "0.2.1-alpha.2",
|
|
4
4
|
"description": "TypeScript SDK for Absurd-SQLite - SQLite-based durable task execution",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"better-sqlite3": "^12.5.0"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"absurd-sdk": "
|
|
39
|
+
"absurd-sdk": "https://github.com/bcho/absurd/releases/download/sdks%2Ftypescript%2Fv0.0.7/typescript-sdk-v0.0.7.tgz"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/better-sqlite3": "^7.6.13",
|
|
@@ -48,4 +48,4 @@
|
|
|
48
48
|
"engines": {
|
|
49
49
|
"node": ">=18.0.0"
|
|
50
50
|
}
|
|
51
|
-
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync, chmodSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
|
|
6
|
+
export interface DownloadExtensionOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Version of the extension to download. If not specified, uses "latest".
|
|
9
|
+
* Examples: "v0.1.0-alpha.3", "latest"
|
|
10
|
+
*/
|
|
11
|
+
version?: string;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* GitHub repository owner. Defaults to "b4fun".
|
|
15
|
+
*/
|
|
16
|
+
owner?: string;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* GitHub repository name. Defaults to "absurd-sqlite".
|
|
20
|
+
*/
|
|
21
|
+
repo?: string;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Custom cache directory for storing downloaded extensions.
|
|
25
|
+
* If not specified, uses a default cache directory in user's home.
|
|
26
|
+
*/
|
|
27
|
+
cacheDir?: string;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Force re-download even if cached version exists.
|
|
31
|
+
*/
|
|
32
|
+
force?: boolean;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Expected SHA256 checksum of the downloaded file for validation.
|
|
36
|
+
* If provided, the downloaded file's checksum will be verified against this value.
|
|
37
|
+
* If the checksum doesn't match, an error will be thrown.
|
|
38
|
+
*/
|
|
39
|
+
expectedChecksum?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface PlatformInfo {
|
|
43
|
+
os: string;
|
|
44
|
+
arch: string;
|
|
45
|
+
ext: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getPlatformInfo(): PlatformInfo {
|
|
49
|
+
const platform = process.platform;
|
|
50
|
+
const arch = process.arch;
|
|
51
|
+
|
|
52
|
+
let os: string;
|
|
53
|
+
let ext: string;
|
|
54
|
+
|
|
55
|
+
switch (platform) {
|
|
56
|
+
case "darwin":
|
|
57
|
+
os = "macOS";
|
|
58
|
+
ext = "dylib";
|
|
59
|
+
break;
|
|
60
|
+
case "linux":
|
|
61
|
+
os = "Linux";
|
|
62
|
+
ext = "so";
|
|
63
|
+
break;
|
|
64
|
+
case "win32":
|
|
65
|
+
os = "Windows";
|
|
66
|
+
ext = "dll";
|
|
67
|
+
break;
|
|
68
|
+
default:
|
|
69
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let archStr: string;
|
|
73
|
+
switch (arch) {
|
|
74
|
+
case "x64":
|
|
75
|
+
archStr = "X64";
|
|
76
|
+
break;
|
|
77
|
+
case "arm64":
|
|
78
|
+
archStr = "ARM64";
|
|
79
|
+
break;
|
|
80
|
+
default:
|
|
81
|
+
throw new Error(`Unsupported architecture: ${arch}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { os, arch: archStr, ext };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getDefaultCacheDir(): string {
|
|
88
|
+
return join(homedir(), ".cache", "absurd-sqlite", "extensions");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getAssetName(version: string, platform: PlatformInfo): string {
|
|
92
|
+
// Format: absurd-absurd-sqlite-extension-vX.Y.Z-{OS}-{ARCH}.{ext}
|
|
93
|
+
return `absurd-absurd-sqlite-extension-${version}-${platform.os}-${platform.arch}.${platform.ext}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getTag(version: string): string {
|
|
97
|
+
return `absurd-sqlite-extension/${version}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function fetchLatestVersion(
|
|
101
|
+
owner: string,
|
|
102
|
+
repo: string
|
|
103
|
+
): Promise<string> {
|
|
104
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/releases`;
|
|
105
|
+
const response = await fetch(url);
|
|
106
|
+
|
|
107
|
+
if (!response.ok) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`Failed to fetch releases: ${response.status} ${response.statusText}`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const releases = (await response.json()) as Array<{
|
|
114
|
+
tag_name: string;
|
|
115
|
+
prerelease: boolean;
|
|
116
|
+
draft: boolean;
|
|
117
|
+
}>;
|
|
118
|
+
|
|
119
|
+
// Find the latest non-draft extension release
|
|
120
|
+
for (const release of releases) {
|
|
121
|
+
if (
|
|
122
|
+
!release.draft &&
|
|
123
|
+
release.tag_name.startsWith("absurd-sqlite-extension/")
|
|
124
|
+
) {
|
|
125
|
+
// Extract version from tag (e.g., "absurd-sqlite-extension/v0.1.0-alpha.3" -> "v0.1.0-alpha.3")
|
|
126
|
+
return release.tag_name.replace("absurd-sqlite-extension/", "");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
throw new Error("No extension releases found");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function downloadAsset(
|
|
134
|
+
owner: string,
|
|
135
|
+
repo: string,
|
|
136
|
+
tag: string,
|
|
137
|
+
assetName: string,
|
|
138
|
+
destPath: string,
|
|
139
|
+
expectedChecksum?: string
|
|
140
|
+
): Promise<void> {
|
|
141
|
+
const url = `https://github.com/${owner}/${repo}/releases/download/${tag}/${assetName}`;
|
|
142
|
+
const response = await fetch(url);
|
|
143
|
+
|
|
144
|
+
if (!response.ok) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`Failed to download extension: ${response.status} ${response.statusText} from ${url}`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const buffer = await response.arrayBuffer();
|
|
151
|
+
writeFileSync(destPath, Buffer.from(buffer));
|
|
152
|
+
|
|
153
|
+
// Verify checksum if provided
|
|
154
|
+
if (expectedChecksum) {
|
|
155
|
+
const actualChecksum = calculateChecksum(destPath);
|
|
156
|
+
if (actualChecksum !== expectedChecksum.toLowerCase()) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
`Checksum verification failed. Expected: ${expectedChecksum.toLowerCase()}, Got: ${actualChecksum}`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Make the extension executable on Unix-like systems
|
|
164
|
+
if (process.platform !== "win32") {
|
|
165
|
+
chmodSync(destPath, 0o755);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function calculateChecksum(filePath: string): string {
|
|
170
|
+
const fileBuffer = readFileSync(filePath);
|
|
171
|
+
const hash = createHash("sha256");
|
|
172
|
+
hash.update(fileBuffer);
|
|
173
|
+
return hash.digest("hex");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function getCachedPath(
|
|
177
|
+
cacheDir: string,
|
|
178
|
+
version: string,
|
|
179
|
+
platform: PlatformInfo
|
|
180
|
+
): string {
|
|
181
|
+
// Use a consistent name that SQLite expects: libabsurd.{ext}
|
|
182
|
+
const ext = platform.ext;
|
|
183
|
+
return join(cacheDir, version, `libabsurd.${ext}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Downloads the absurd-sqlite extension from GitHub releases.
|
|
188
|
+
* Returns the path to the downloaded extension file.
|
|
189
|
+
*
|
|
190
|
+
* @param options - Download options
|
|
191
|
+
* @returns Path to the extension file
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* ```typescript
|
|
195
|
+
* import { downloadExtension } from "@absurd-sqlite/sdk";
|
|
196
|
+
*
|
|
197
|
+
* // Download latest version
|
|
198
|
+
* const extensionPath = await downloadExtension();
|
|
199
|
+
*
|
|
200
|
+
* // Download specific version
|
|
201
|
+
* const extensionPath = await downloadExtension({ version: "v0.1.0-alpha.3" });
|
|
202
|
+
*
|
|
203
|
+
* // Download with checksum verification
|
|
204
|
+
* const extensionPath = await downloadExtension({
|
|
205
|
+
* version: "v0.1.0-alpha.3",
|
|
206
|
+
* expectedChecksum: "abc123..."
|
|
207
|
+
* });
|
|
208
|
+
* ```
|
|
209
|
+
*/
|
|
210
|
+
export async function downloadExtension(
|
|
211
|
+
options: DownloadExtensionOptions = {}
|
|
212
|
+
): Promise<string> {
|
|
213
|
+
const owner = options.owner ?? "b4fun";
|
|
214
|
+
const repo = options.repo ?? "absurd-sqlite";
|
|
215
|
+
const cacheDir = options.cacheDir ?? getDefaultCacheDir();
|
|
216
|
+
const force = options.force ?? false;
|
|
217
|
+
|
|
218
|
+
// Resolve version
|
|
219
|
+
let version = options.version ?? "latest";
|
|
220
|
+
if (version === "latest") {
|
|
221
|
+
version = await fetchLatestVersion(owner, repo);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Get platform info
|
|
225
|
+
const platform = getPlatformInfo();
|
|
226
|
+
const assetName = getAssetName(version, platform);
|
|
227
|
+
const tag = getTag(version);
|
|
228
|
+
|
|
229
|
+
// Check cache
|
|
230
|
+
const cachedPath = getCachedPath(cacheDir, version, platform);
|
|
231
|
+
|
|
232
|
+
if (!force && existsSync(cachedPath)) {
|
|
233
|
+
// If checksum is provided, verify cached file
|
|
234
|
+
if (options.expectedChecksum) {
|
|
235
|
+
const actualChecksum = calculateChecksum(cachedPath);
|
|
236
|
+
if (actualChecksum !== options.expectedChecksum.toLowerCase()) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
`Cached file checksum verification failed. Expected: ${options.expectedChecksum.toLowerCase()}, Got: ${actualChecksum}`
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return cachedPath;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Ensure cache directory exists
|
|
246
|
+
const versionDir = join(cacheDir, version);
|
|
247
|
+
mkdirSync(versionDir, { recursive: true });
|
|
248
|
+
|
|
249
|
+
// Download asset
|
|
250
|
+
await downloadAsset(owner, repo, tag, assetName, cachedPath, options.expectedChecksum);
|
|
251
|
+
|
|
252
|
+
return cachedPath;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Resolves the extension path, either from the provided path, environment variable,
|
|
257
|
+
* or by downloading from GitHub releases.
|
|
258
|
+
*
|
|
259
|
+
* @param extensionPath - Optional path to the extension file
|
|
260
|
+
* @param downloadOptions - Options for downloading if no path is provided
|
|
261
|
+
* @returns Path to the extension file
|
|
262
|
+
*
|
|
263
|
+
* @example
|
|
264
|
+
* ```typescript
|
|
265
|
+
* import { resolveExtensionPath } from "@absurd-sqlite/sdk";
|
|
266
|
+
*
|
|
267
|
+
* // Use provided path
|
|
268
|
+
* const path1 = await resolveExtensionPath("/path/to/extension.so");
|
|
269
|
+
*
|
|
270
|
+
* // Use environment variable or download
|
|
271
|
+
* const path2 = await resolveExtensionPath();
|
|
272
|
+
* ```
|
|
273
|
+
*/
|
|
274
|
+
export async function resolveExtensionPath(
|
|
275
|
+
extensionPath?: string,
|
|
276
|
+
downloadOptions?: DownloadExtensionOptions
|
|
277
|
+
): Promise<string> {
|
|
278
|
+
// If path is provided, use it
|
|
279
|
+
if (extensionPath) {
|
|
280
|
+
return extensionPath;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Try environment variable
|
|
284
|
+
const envPath = process.env.ABSURD_SQLITE_EXTENSION_PATH;
|
|
285
|
+
if (envPath) {
|
|
286
|
+
return envPath;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Download from GitHub releases
|
|
290
|
+
return downloadExtension(downloadOptions);
|
|
291
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,11 @@ import type { SQLiteDatabase } from "./sqlite-types";
|
|
|
5
5
|
import { SqliteConnection } from "./sqlite";
|
|
6
6
|
|
|
7
7
|
export type { AbsurdClient, Queryable, Worker } from "./absurd-types";
|
|
8
|
+
export {
|
|
9
|
+
downloadExtension,
|
|
10
|
+
resolveExtensionPath,
|
|
11
|
+
type DownloadExtensionOptions,
|
|
12
|
+
} from "./extension-downloader";
|
|
8
13
|
export type {
|
|
9
14
|
AbsurdHooks,
|
|
10
15
|
AbsurdOptions,
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
downloadExtension,
|
|
8
|
+
resolveExtensionPath,
|
|
9
|
+
} from "../src/extension-downloader";
|
|
10
|
+
|
|
11
|
+
let tempCacheDir: string | null = null;
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
if (tempCacheDir) {
|
|
15
|
+
rmSync(tempCacheDir, { recursive: true, force: true });
|
|
16
|
+
tempCacheDir = null;
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Helper function to calculate checksum of a file
|
|
21
|
+
async function getFileChecksum(filePath: string): Promise<string> {
|
|
22
|
+
const { createHash } = await import("node:crypto");
|
|
23
|
+
const { readFileSync } = await import("node:fs");
|
|
24
|
+
const fileBuffer = readFileSync(filePath);
|
|
25
|
+
const hash = createHash("sha256");
|
|
26
|
+
hash.update(fileBuffer);
|
|
27
|
+
return hash.digest("hex");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("downloadExtension", () => {
|
|
31
|
+
it("downloads specific version", async () => {
|
|
32
|
+
tempCacheDir = mkdtempSync(join(tmpdir(), "absurd-ext-cache-"));
|
|
33
|
+
|
|
34
|
+
const extensionPath = await downloadExtension({
|
|
35
|
+
version: "v0.1.0-alpha.3",
|
|
36
|
+
cacheDir: tempCacheDir,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(existsSync(extensionPath)).toBe(true);
|
|
40
|
+
expect(extensionPath).toContain("v0.1.0-alpha.3");
|
|
41
|
+
|
|
42
|
+
// Verify it has the right extension
|
|
43
|
+
const platform = process.platform;
|
|
44
|
+
if (platform === "darwin") {
|
|
45
|
+
expect(extensionPath).toMatch(/libabsurd\.dylib$/);
|
|
46
|
+
} else if (platform === "linux") {
|
|
47
|
+
expect(extensionPath).toMatch(/libabsurd\.so$/);
|
|
48
|
+
} else if (platform === "win32") {
|
|
49
|
+
expect(extensionPath).toMatch(/libabsurd\.dll$/);
|
|
50
|
+
}
|
|
51
|
+
}, 60000);
|
|
52
|
+
|
|
53
|
+
it("uses cached version on second call", async () => {
|
|
54
|
+
tempCacheDir = mkdtempSync(join(tmpdir(), "absurd-ext-cache-"));
|
|
55
|
+
|
|
56
|
+
const path1 = await downloadExtension({
|
|
57
|
+
version: "v0.1.0-alpha.3",
|
|
58
|
+
cacheDir: tempCacheDir,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const path2 = await downloadExtension({
|
|
62
|
+
version: "v0.1.0-alpha.3",
|
|
63
|
+
cacheDir: tempCacheDir,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
expect(path1).toBe(path2);
|
|
67
|
+
}, 60000);
|
|
68
|
+
|
|
69
|
+
it("forces re-download when force=true", async () => {
|
|
70
|
+
tempCacheDir = mkdtempSync(join(tmpdir(), "absurd-ext-cache-"));
|
|
71
|
+
|
|
72
|
+
const path1 = await downloadExtension({
|
|
73
|
+
version: "v0.1.0-alpha.3",
|
|
74
|
+
cacheDir: tempCacheDir,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Delete the cached file
|
|
78
|
+
rmSync(path1);
|
|
79
|
+
expect(existsSync(path1)).toBe(false);
|
|
80
|
+
|
|
81
|
+
// Download again with force
|
|
82
|
+
const path2 = await downloadExtension({
|
|
83
|
+
version: "v0.1.0-alpha.3",
|
|
84
|
+
cacheDir: tempCacheDir,
|
|
85
|
+
force: true,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
expect(existsSync(path2)).toBe(true);
|
|
89
|
+
expect(path2).toBe(path1); // Same path
|
|
90
|
+
}, 60000);
|
|
91
|
+
|
|
92
|
+
it("validates checksum when provided", async () => {
|
|
93
|
+
tempCacheDir = mkdtempSync(join(tmpdir(), "absurd-ext-cache-"));
|
|
94
|
+
|
|
95
|
+
// First download to get the file
|
|
96
|
+
const extensionPath = await downloadExtension({
|
|
97
|
+
version: "v0.1.0-alpha.3",
|
|
98
|
+
cacheDir: tempCacheDir,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Calculate the actual checksum
|
|
102
|
+
const actualChecksum = await getFileChecksum(extensionPath);
|
|
103
|
+
|
|
104
|
+
// Clear cache
|
|
105
|
+
rmSync(extensionPath);
|
|
106
|
+
|
|
107
|
+
// Download again with correct checksum - should succeed
|
|
108
|
+
const path1 = await downloadExtension({
|
|
109
|
+
version: "v0.1.0-alpha.3",
|
|
110
|
+
cacheDir: tempCacheDir,
|
|
111
|
+
expectedChecksum: actualChecksum,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
expect(existsSync(path1)).toBe(true);
|
|
115
|
+
|
|
116
|
+
// Try with wrong checksum - should fail
|
|
117
|
+
rmSync(path1);
|
|
118
|
+
await expect(
|
|
119
|
+
downloadExtension({
|
|
120
|
+
version: "v0.1.0-alpha.3",
|
|
121
|
+
cacheDir: tempCacheDir,
|
|
122
|
+
expectedChecksum: "0000000000000000000000000000000000000000000000000000000000000000",
|
|
123
|
+
})
|
|
124
|
+
).rejects.toThrow(/Checksum verification failed/);
|
|
125
|
+
}, 60000);
|
|
126
|
+
|
|
127
|
+
it("validates checksum of cached file", async () => {
|
|
128
|
+
tempCacheDir = mkdtempSync(join(tmpdir(), "absurd-ext-cache-"));
|
|
129
|
+
|
|
130
|
+
// First download to get the file
|
|
131
|
+
const extensionPath = await downloadExtension({
|
|
132
|
+
version: "v0.1.0-alpha.3",
|
|
133
|
+
cacheDir: tempCacheDir,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Calculate the actual checksum
|
|
137
|
+
const actualChecksum = await getFileChecksum(extensionPath);
|
|
138
|
+
|
|
139
|
+
// Use cached file with correct checksum - should succeed
|
|
140
|
+
const path1 = await downloadExtension({
|
|
141
|
+
version: "v0.1.0-alpha.3",
|
|
142
|
+
cacheDir: tempCacheDir,
|
|
143
|
+
expectedChecksum: actualChecksum,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(existsSync(path1)).toBe(true);
|
|
147
|
+
|
|
148
|
+
// Try with wrong checksum on cached file - should fail
|
|
149
|
+
await expect(
|
|
150
|
+
downloadExtension({
|
|
151
|
+
version: "v0.1.0-alpha.3",
|
|
152
|
+
cacheDir: tempCacheDir,
|
|
153
|
+
expectedChecksum: "0000000000000000000000000000000000000000000000000000000000000000",
|
|
154
|
+
})
|
|
155
|
+
).rejects.toThrow(/Cached file checksum verification failed/);
|
|
156
|
+
}, 60000);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("resolveExtensionPath", () => {
|
|
160
|
+
it("returns provided path when given", async () => {
|
|
161
|
+
const providedPath = "/custom/path/to/extension.so";
|
|
162
|
+
const resolved = await resolveExtensionPath(providedPath);
|
|
163
|
+
expect(resolved).toBe(providedPath);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("uses environment variable when no path provided", async () => {
|
|
167
|
+
const envPath = "/env/path/to/extension.so";
|
|
168
|
+
const originalEnv = process.env.ABSURD_SQLITE_EXTENSION_PATH;
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
process.env.ABSURD_SQLITE_EXTENSION_PATH = envPath;
|
|
172
|
+
const resolved = await resolveExtensionPath();
|
|
173
|
+
expect(resolved).toBe(envPath);
|
|
174
|
+
} finally {
|
|
175
|
+
if (originalEnv !== undefined) {
|
|
176
|
+
process.env.ABSURD_SQLITE_EXTENSION_PATH = originalEnv;
|
|
177
|
+
} else {
|
|
178
|
+
delete process.env.ABSURD_SQLITE_EXTENSION_PATH;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("downloads when no path or env var provided", async () => {
|
|
184
|
+
tempCacheDir = mkdtempSync(join(tmpdir(), "absurd-ext-cache-"));
|
|
185
|
+
const originalEnv = process.env.ABSURD_SQLITE_EXTENSION_PATH;
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
delete process.env.ABSURD_SQLITE_EXTENSION_PATH;
|
|
189
|
+
|
|
190
|
+
const resolved = await resolveExtensionPath(undefined, {
|
|
191
|
+
version: "v0.1.0-alpha.3",
|
|
192
|
+
cacheDir: tempCacheDir,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
expect(existsSync(resolved)).toBe(true);
|
|
196
|
+
expect(resolved).toContain(tempCacheDir);
|
|
197
|
+
} finally {
|
|
198
|
+
if (originalEnv !== undefined) {
|
|
199
|
+
process.env.ABSURD_SQLITE_EXTENSION_PATH = originalEnv;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}, 60000);
|
|
203
|
+
});
|