@wervt/nuxt 0.2.1 → 0.2.5
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/dev.d.ts +4 -0
- package/dist/dev.js +33 -4
- package/dist/module.d.ts +14 -0
- package/dist/module.js +87 -4
- package/dist/node_modules/.pnpm/h3@2.0.1_crossws@0.4.12_srvx@1.0.5__ocache@0.3.0/node_modules/h3/dist/response.js +96 -0
- package/dist/node_modules/.pnpm/nitro@3.0.260903-beta/node_modules/nitro/dist/runtime/internal/task.js +29 -0
- package/dist/node_modules/.pnpm/nitro@3.0.260903-beta/node_modules/nitro/dist/runtime/task.js +2 -0
- package/dist/provision.d.ts +9 -1
- package/dist/provision.js +10 -1
- package/dist/runtime/server/index.d.ts +2 -1
- package/dist/runtime/server/index.js +2 -1
- package/dist/runtime/server/storage-plugin.d.ts +3 -0
- package/dist/runtime/server/storage-plugin.js +16 -0
- package/dist/runtime/server/task-handlers.d.ts +28 -0
- package/dist/runtime/server/task-handlers.js +38 -0
- package/dist/runtime/server/task-list-nitro2.d.ts +9 -0
- package/dist/runtime/server/task-list-nitro2.js +6 -0
- package/dist/runtime/server/task-list-nitro3.d.ts +9 -0
- package/dist/runtime/server/task-list-nitro3.js +6 -0
- package/dist/runtime/server/task-run-nitro2.d.ts +10 -0
- package/dist/runtime/server/task-run-nitro2.js +6 -0
- package/dist/runtime/server/task-run-nitro3.d.ts +10 -0
- package/dist/runtime/server/task-run-nitro3.js +7 -0
- package/dist/runtime/server/tasks.d.ts +20 -0
- package/dist/runtime/server/tasks.js +10 -0
- package/dist/storage.d.ts +26 -0
- package/dist/storage.js +144 -0
- package/package.json +2 -1
package/dist/dev.d.ts
CHANGED
|
@@ -7,6 +7,10 @@ export interface DevOptions {
|
|
|
7
7
|
dbSecret?: string;
|
|
8
8
|
electricUrl?: string;
|
|
9
9
|
electricSecret?: string;
|
|
10
|
+
/** The local Garage, for apps with ablage: S3 API and admin API. */
|
|
11
|
+
s3Url?: string;
|
|
12
|
+
garageAdminUrl?: string;
|
|
13
|
+
garageAdminToken?: string;
|
|
10
14
|
/**
|
|
11
15
|
* Who `/_wervt/login` signs in as during `nuxt dev` (there's no wervt auth locally).
|
|
12
16
|
* Defaults to a dev user with a Twitch account linked.
|
package/dist/dev.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createStorage } from "./storage.js";
|
|
2
|
+
import { appDatabaseUrl, dbRole, provisionDatabase, storageCredentials } from "./provision.js";
|
|
2
3
|
import { useLogger } from "nuxt/kit";
|
|
3
|
-
import { existsSync, watch } from "node:fs";
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
+
import { existsSync, watch } from "node:fs";
|
|
6
7
|
//#region src/dev.ts
|
|
7
8
|
const DEFAULTS = {
|
|
8
9
|
adminUrl: "postgres://postgres:postgres@localhost:5432/wervt",
|
|
9
10
|
dbSecret: "dev-secret",
|
|
10
11
|
electricUrl: "http://localhost:3030",
|
|
11
|
-
electricSecret: "dev-electric-secret"
|
|
12
|
+
electricSecret: "dev-electric-secret",
|
|
13
|
+
s3Url: "http://localhost:3900",
|
|
14
|
+
garageAdminUrl: "http://localhost:3903",
|
|
15
|
+
garageAdminToken: "dev-garage-admin"
|
|
12
16
|
};
|
|
13
17
|
const logger = useLogger("wervt");
|
|
14
18
|
/**
|
|
@@ -16,7 +20,7 @@ const logger = useLogger("wervt");
|
|
|
16
20
|
* runs its migrations (again whenever `drizzle/` changes) and points the dev server
|
|
17
21
|
* at the app's database and Electric.
|
|
18
22
|
*/
|
|
19
|
-
async function setupDev(nuxt, app, options) {
|
|
23
|
+
async function setupDev(nuxt, app, options, features = {}) {
|
|
20
24
|
const config = {
|
|
21
25
|
adminUrl: process.env.WERVT_PG_ADMIN_URL ?? options.adminUrl ?? DEFAULTS.adminUrl,
|
|
22
26
|
dbSecret: process.env.WERVT_DB_SECRET ?? options.dbSecret ?? DEFAULTS.dbSecret,
|
|
@@ -29,6 +33,7 @@ async function setupDev(nuxt, app, options) {
|
|
|
29
33
|
name: "Dev",
|
|
30
34
|
accounts: { twitch: "dev" }
|
|
31
35
|
});
|
|
36
|
+
if (features.storage) await setupStorage(app, config.dbSecret, options);
|
|
32
37
|
const migrationsFolder = join(nuxt.options.rootDir, "drizzle");
|
|
33
38
|
if (!existsSync(migrationsFolder)) return;
|
|
34
39
|
const provision = {
|
|
@@ -61,6 +66,30 @@ async function setupDev(nuxt, app, options) {
|
|
|
61
66
|
});
|
|
62
67
|
nuxt.hook("close", () => watcher.close());
|
|
63
68
|
}
|
|
69
|
+
/** The app's bucket in the local Garage, and WERVT_S3_* for ablage, like in production. */
|
|
70
|
+
async function setupStorage(app, secret, options) {
|
|
71
|
+
const s3Url = process.env.WERVT_S3_URL ?? options.s3Url ?? DEFAULTS.s3Url;
|
|
72
|
+
const storage = createStorage({
|
|
73
|
+
adminUrl: options.garageAdminUrl ?? DEFAULTS.garageAdminUrl,
|
|
74
|
+
adminToken: process.env.WERVT_GARAGE_ADMIN_TOKEN ?? options.garageAdminToken ?? DEFAULTS.garageAdminToken,
|
|
75
|
+
s3Url,
|
|
76
|
+
secret
|
|
77
|
+
});
|
|
78
|
+
try {
|
|
79
|
+
await storage.ensureLayout();
|
|
80
|
+
const { bucket } = await storage.provision(app, "*");
|
|
81
|
+
logger.success(`Storage bucket \`${bucket}\` is ready`);
|
|
82
|
+
} catch (e) {
|
|
83
|
+
return logger.warn("Could not set up storage. Is the wervt stack running (docker compose up -d)?", e instanceof Error ? e.message : e);
|
|
84
|
+
}
|
|
85
|
+
const creds = await storageCredentials(app, secret);
|
|
86
|
+
process.env.WERVT_S3_URL ??= s3Url;
|
|
87
|
+
process.env.WERVT_S3_PUBLIC_URL ??= s3Url;
|
|
88
|
+
process.env.WERVT_S3_REGION ??= "garage";
|
|
89
|
+
process.env.WERVT_S3_BUCKET ??= creds.bucket;
|
|
90
|
+
process.env.WERVT_S3_ACCESS_KEY_ID ??= creds.accessKeyId;
|
|
91
|
+
process.env.WERVT_S3_SECRET_ACCESS_KEY ??= creds.secretAccessKey;
|
|
92
|
+
}
|
|
64
93
|
/** App name from the module option or package.json, normalised to `[a-z0-9-]+`. */
|
|
65
94
|
async function resolveAppName(rootDir, name) {
|
|
66
95
|
if (!name) {
|
package/dist/module.d.ts
CHANGED
|
@@ -5,6 +5,20 @@ export interface ModuleOptions {
|
|
|
5
5
|
app?: string;
|
|
6
6
|
/** Local stack used by `nuxt dev`. Defaults match the wervt compose setup. */
|
|
7
7
|
dev?: DevOptions;
|
|
8
|
+
/** Time zone of `nitro.scheduledTasks`, e.g. `Europe/Berlin`. Defaults to UTC. */
|
|
9
|
+
timezone?: string;
|
|
10
|
+
}
|
|
11
|
+
/** `server/wervt-tasks.json` in the build: the app's tasks and schedules, for the control plane. */
|
|
12
|
+
export interface TasksManifest {
|
|
13
|
+
tasks: {
|
|
14
|
+
name: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
}[];
|
|
17
|
+
schedules: {
|
|
18
|
+
cron: string;
|
|
19
|
+
tasks: string[];
|
|
20
|
+
}[];
|
|
21
|
+
timezone?: string;
|
|
8
22
|
}
|
|
9
23
|
declare const _default: import("nuxt/schema").NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
10
24
|
//#endregion
|
package/dist/module.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { resolveAppName, setupDev } from "./dev.js";
|
|
2
|
-
import { addImports, addPlugin, addServerHandler, createResolver, defineNuxtModule, hasNuxtModule, installModule } from "nuxt/kit";
|
|
3
|
-
import {
|
|
2
|
+
import { addImports, addPlugin, addServerHandler, addServerPlugin, createResolver, defineNuxtModule, getNuxtVersion, hasNuxtModule, installModule } from "nuxt/kit";
|
|
3
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
4
|
+
import { builtinModules, createRequire } from "node:module";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
4
6
|
import evlog from "evlog/nuxt";
|
|
5
7
|
//#region src/module.ts
|
|
6
8
|
const CONTENT_DATABASE = {
|
|
@@ -67,7 +69,11 @@ var module_default = defineNuxtModule({
|
|
|
67
69
|
}
|
|
68
70
|
if (!nuxt.options.dev) options.rollupConfig = {
|
|
69
71
|
...options.rollupConfig,
|
|
70
|
-
plugins: [
|
|
72
|
+
plugins: [
|
|
73
|
+
...[options.rollupConfig?.plugins ?? []].flat(),
|
|
74
|
+
prefixNodeBuiltins(),
|
|
75
|
+
configurableWaitUntil()
|
|
76
|
+
]
|
|
71
77
|
};
|
|
72
78
|
options.unenv = [...options.unenv ? [options.unenv].flat() : [], { inject: {
|
|
73
79
|
Buffer: ["node:buffer", "Buffer"],
|
|
@@ -75,9 +81,65 @@ var module_default = defineNuxtModule({
|
|
|
75
81
|
clearImmediate: ["node:timers", "clearImmediate"]
|
|
76
82
|
} }];
|
|
77
83
|
});
|
|
84
|
+
const useAblage = hasNuxtModule("ablage", nuxt);
|
|
85
|
+
if (useAblage) {
|
|
86
|
+
const options = nuxt.options;
|
|
87
|
+
options.ablage = {
|
|
88
|
+
...options.ablage,
|
|
89
|
+
provider: options.ablage?.provider ?? "custom"
|
|
90
|
+
};
|
|
91
|
+
addServerPlugin(resolver.resolve("./runtime/server/storage-plugin"));
|
|
92
|
+
const require = createRequire(import.meta.url);
|
|
93
|
+
const aws4fetch = join(dirname(require.resolve("aws4fetch")), "aws4fetch.esm.mjs");
|
|
94
|
+
nuxt.hook("nitro:config", (config) => {
|
|
95
|
+
const options = config;
|
|
96
|
+
options.alias = {
|
|
97
|
+
...options.alias,
|
|
98
|
+
aws4fetch
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
nitro.experimental = {
|
|
103
|
+
...nitro.experimental,
|
|
104
|
+
tasks: true
|
|
105
|
+
};
|
|
106
|
+
const nitroVersion = Number.parseInt(getNuxtVersion(nuxt), 10) >= 5 ? "nitro3" : "nitro2";
|
|
107
|
+
addServerHandler({
|
|
108
|
+
route: "/_wervt/tasks/**",
|
|
109
|
+
method: "post",
|
|
110
|
+
handler: resolver.resolve(`./runtime/server/task-run-${nitroVersion}`)
|
|
111
|
+
});
|
|
112
|
+
addServerHandler({
|
|
113
|
+
route: "/_wervt/tasks",
|
|
114
|
+
method: "get",
|
|
115
|
+
handler: resolver.resolve(`./runtime/server/task-list-${nitroVersion}`)
|
|
116
|
+
});
|
|
117
|
+
if (!nuxt.options.dev) {
|
|
118
|
+
const timezone = options.timezone;
|
|
119
|
+
nuxt.hook("nitro:init", (instance) => {
|
|
120
|
+
const nitroOptions = instance.options;
|
|
121
|
+
const schedules = Object.entries(nitroOptions.scheduledTasks ?? {}).map(([cron, tasks]) => ({
|
|
122
|
+
cron,
|
|
123
|
+
tasks: [tasks].flat()
|
|
124
|
+
}));
|
|
125
|
+
nitroOptions.scheduledTasks = {};
|
|
126
|
+
instance.hooks.hook("compiled", async () => {
|
|
127
|
+
const manifest = {
|
|
128
|
+
tasks: Object.entries(nitroOptions.tasks ?? {}).map(([name, task]) => ({
|
|
129
|
+
name,
|
|
130
|
+
description: task.description || void 0
|
|
131
|
+
})),
|
|
132
|
+
schedules,
|
|
133
|
+
timezone
|
|
134
|
+
};
|
|
135
|
+
await mkdir(nitroOptions.output.serverDir, { recursive: true });
|
|
136
|
+
await writeFile(join(nitroOptions.output.serverDir, "wervt-tasks.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
}
|
|
78
140
|
if (nuxt.options.dev && !nuxt.options._prepare) {
|
|
79
141
|
const app = await resolveAppName(nuxt.options.rootDir, options.app);
|
|
80
|
-
await setupDev(nuxt, app, options.dev ?? {});
|
|
142
|
+
await setupDev(nuxt, app, options.dev ?? {}, { storage: useAblage });
|
|
81
143
|
}
|
|
82
144
|
if (nuxt.options.dev) for (const route of ["/_wervt/login", "/_wervt/logout"]) addServerHandler({
|
|
83
145
|
route,
|
|
@@ -133,6 +195,27 @@ function prefixNodeBuiltins() {
|
|
|
133
195
|
}
|
|
134
196
|
};
|
|
135
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* srvx (Nitro 3's server layer) pins `waitUntil` on each request as a non-configurable property.
|
|
200
|
+
* Edge Runtime hands the app a Proxy whose `get` returns its own `waitUntil`, which breaks the
|
|
201
|
+
* proxy invariant for non-configurable properties: reading `req.waitUntil` throws. Nitro's
|
|
202
|
+
* error handler reads it, so every thrown error (404, 400, …) became a 500. Configurable, the
|
|
203
|
+
* proxy may answer with its own function, which works the same.
|
|
204
|
+
*/
|
|
205
|
+
function configurableWaitUntil() {
|
|
206
|
+
const PIN_RE = /waitUntil:\s*\{\s*value:\s*(this\.#wait\?\.waitUntil)\s*\}/g;
|
|
207
|
+
return {
|
|
208
|
+
name: "wervt:configurable-wait-until",
|
|
209
|
+
renderChunk(code) {
|
|
210
|
+
if (!code.includes("waitUntil")) return null;
|
|
211
|
+
const out = code.replace(PIN_RE, "waitUntil: { value: $1, configurable: true, writable: true }");
|
|
212
|
+
return out === code ? null : {
|
|
213
|
+
code: out,
|
|
214
|
+
map: null
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
}
|
|
136
219
|
/** `pattern`, except for files of this module's runtime. */
|
|
137
220
|
function exceptRuntime(pattern) {
|
|
138
221
|
if (pattern.source.startsWith("^")) return pattern;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
String.raw`%(?:2[146-9A-E]|3[0-9ABD]|4[0-9A-F]|5[0-9ABDF]|6[1-9A-F]|7[0-9ACE])`;
|
|
2
|
+
const DISALLOWED_STATUS_CHARS = /[^\u0009\u0020-\u007E]/g;
|
|
3
|
+
function sanitizeStatusMessage(statusMessage = "") {
|
|
4
|
+
return statusMessage.replace(DISALLOWED_STATUS_CHARS, "");
|
|
5
|
+
}
|
|
6
|
+
function sanitizeStatusCode(statusCode, defaultStatusCode = 200) {
|
|
7
|
+
if (!statusCode) return defaultStatusCode;
|
|
8
|
+
if (typeof statusCode === "string") statusCode = +statusCode;
|
|
9
|
+
if (!Number.isInteger(statusCode) || statusCode < 100 || statusCode > 599) return defaultStatusCode;
|
|
10
|
+
return statusCode;
|
|
11
|
+
}
|
|
12
|
+
var HTTPError = class HTTPError extends Error {
|
|
13
|
+
get name() {
|
|
14
|
+
return "HTTPError";
|
|
15
|
+
}
|
|
16
|
+
status;
|
|
17
|
+
statusText;
|
|
18
|
+
headers;
|
|
19
|
+
cause;
|
|
20
|
+
data;
|
|
21
|
+
body;
|
|
22
|
+
unhandled;
|
|
23
|
+
static isError(input) {
|
|
24
|
+
return input instanceof Error && input?.name === "HTTPError" && input.status > 99;
|
|
25
|
+
}
|
|
26
|
+
static status(status, statusText, details) {
|
|
27
|
+
return new HTTPError({
|
|
28
|
+
...details,
|
|
29
|
+
statusText,
|
|
30
|
+
status
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
constructor(arg1, arg2) {
|
|
34
|
+
let messageInput;
|
|
35
|
+
let details;
|
|
36
|
+
if (typeof arg1 === "string") {
|
|
37
|
+
messageInput = arg1;
|
|
38
|
+
details = arg2;
|
|
39
|
+
} else details = arg1;
|
|
40
|
+
const status = sanitizeStatusCode(details?.status || details?.statusCode || (details?.cause)?.status || (details?.cause)?.statusCode, 500);
|
|
41
|
+
const statusText = sanitizeStatusMessage(details?.statusText || details?.statusMessage || (details?.cause)?.statusText || (details?.cause)?.statusMessage);
|
|
42
|
+
const message = messageInput || details?.message || (details?.cause)?.message || details?.statusText || details?.statusMessage || [
|
|
43
|
+
"HTTPError",
|
|
44
|
+
status,
|
|
45
|
+
statusText
|
|
46
|
+
].filter(Boolean).join(" ");
|
|
47
|
+
super(message, { cause: details });
|
|
48
|
+
this.cause = details;
|
|
49
|
+
this.status = status;
|
|
50
|
+
this.statusText = statusText || void 0;
|
|
51
|
+
const rawHeaders = details?.headers || (details?.cause)?.headers;
|
|
52
|
+
this.headers = rawHeaders ? new Headers(rawHeaders) : void 0;
|
|
53
|
+
this.unhandled = details?.unhandled ?? (details?.cause)?.unhandled ?? void 0;
|
|
54
|
+
this.data = details?.data;
|
|
55
|
+
this.body = details?.body;
|
|
56
|
+
}
|
|
57
|
+
get statusCode() {
|
|
58
|
+
return this.status;
|
|
59
|
+
}
|
|
60
|
+
get statusMessage() {
|
|
61
|
+
return this.statusText;
|
|
62
|
+
}
|
|
63
|
+
toJSON() {
|
|
64
|
+
const unhandled = this.unhandled;
|
|
65
|
+
return {
|
|
66
|
+
status: this.status,
|
|
67
|
+
statusText: this.statusText,
|
|
68
|
+
unhandled,
|
|
69
|
+
message: unhandled ? "HTTPError" : this.message,
|
|
70
|
+
data: unhandled ? void 0 : this.data,
|
|
71
|
+
...unhandled ? void 0 : this.body
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const kHTTPResponse = /* @__PURE__ */ Symbol.for("h3.HTTPResponse");
|
|
76
|
+
var HTTPResponse = class {
|
|
77
|
+
#headers;
|
|
78
|
+
#init;
|
|
79
|
+
body;
|
|
80
|
+
constructor(body, init) {
|
|
81
|
+
this.body = body;
|
|
82
|
+
this.#init = init;
|
|
83
|
+
}
|
|
84
|
+
get status() {
|
|
85
|
+
return this.#init?.status;
|
|
86
|
+
}
|
|
87
|
+
get statusText() {
|
|
88
|
+
return this.#init?.statusText;
|
|
89
|
+
}
|
|
90
|
+
get headers() {
|
|
91
|
+
return this.#headers ||= new Headers(this.#init?.headers);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
HTTPResponse.prototype[kHTTPResponse] = true;
|
|
95
|
+
//#endregion
|
|
96
|
+
export { HTTPError, HTTPResponse, sanitizeStatusCode, sanitizeStatusMessage };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { HTTPError } from "../../../../../../h3@2.0.1_crossws@0.4.12_srvx@1.0.5__ocache@0.3.0/node_modules/h3/dist/response.js";
|
|
2
|
+
import { tasks } from "#nitro/virtual/tasks";
|
|
3
|
+
//#region ../../node_modules/.pnpm/nitro@3.0.260903-beta/node_modules/nitro/dist/runtime/internal/task.mjs
|
|
4
|
+
const __runningTasks__ = {};
|
|
5
|
+
async function runTask(name, { payload = {}, context = {} } = {}) {
|
|
6
|
+
if (__runningTasks__[name]) return __runningTasks__[name];
|
|
7
|
+
if (!(name in tasks)) throw new HTTPError({
|
|
8
|
+
message: `Task \`${name}\` is not available!`,
|
|
9
|
+
status: 404
|
|
10
|
+
});
|
|
11
|
+
if (!tasks[name].resolve) throw new HTTPError({
|
|
12
|
+
message: `Task \`${name}\` is not implemented!`,
|
|
13
|
+
status: 501
|
|
14
|
+
});
|
|
15
|
+
const handler = await tasks[name].resolve();
|
|
16
|
+
const taskEvent = {
|
|
17
|
+
name,
|
|
18
|
+
payload,
|
|
19
|
+
context
|
|
20
|
+
};
|
|
21
|
+
__runningTasks__[name] = handler.run(taskEvent);
|
|
22
|
+
try {
|
|
23
|
+
return await __runningTasks__[name];
|
|
24
|
+
} finally {
|
|
25
|
+
delete __runningTasks__[name];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
export { runTask };
|
package/dist/provision.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { StorageError, StorageOptions, StorageUsage, createStorage } from "./storage.js";
|
|
1
2
|
//#region src/provision.d.ts
|
|
2
3
|
export interface ProvisionOptions {
|
|
3
4
|
/** App name, `[a-z0-9-]+`. */
|
|
@@ -11,6 +12,12 @@ export interface ProvisionOptions {
|
|
|
11
12
|
}
|
|
12
13
|
export declare function dbRole(app: string): string;
|
|
13
14
|
export declare function hmac(secret: string, message: string): Promise<string>;
|
|
15
|
+
/** The app's Garage bucket and S3 key, derived from WERVT_DB_SECRET like role passwords. */
|
|
16
|
+
export declare function storageCredentials(app: string, secret: string): Promise<{
|
|
17
|
+
bucket: string;
|
|
18
|
+
accessKeyId: string;
|
|
19
|
+
secretAccessKey: string;
|
|
20
|
+
}>;
|
|
14
21
|
export declare function dbPassword(app: string, secret: string): Promise<string>;
|
|
15
22
|
/** Connection URL for the app's own role, on the same host and database as `adminUrl`. */
|
|
16
23
|
export declare function appDatabaseUrl({ app, adminUrl, secret }: ProvisionOptions): Promise<string>;
|
|
@@ -23,4 +30,5 @@ export declare function appDatabaseUrl({ app, adminUrl, secret }: ProvisionOptio
|
|
|
23
30
|
export declare function provisionDatabase(options: ProvisionOptions): Promise<void>;
|
|
24
31
|
/** Drops the app's schema (all its data) and role. */
|
|
25
32
|
export declare function dropDatabase({ app, adminUrl }: Pick<ProvisionOptions, 'app' | 'adminUrl'>): Promise<void>;
|
|
26
|
-
//#endregion
|
|
33
|
+
//#endregion
|
|
34
|
+
export { StorageError, type StorageOptions, type StorageUsage, createStorage };
|
package/dist/provision.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { StorageError, createStorage } from "./storage.js";
|
|
1
2
|
import { drizzle } from "drizzle-orm/postgres-js";
|
|
2
3
|
import { migrate } from "drizzle-orm/postgres-js/migrator";
|
|
3
4
|
import postgres from "postgres";
|
|
@@ -13,6 +14,14 @@ async function hmac(secret, message) {
|
|
|
13
14
|
const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(message));
|
|
14
15
|
return Array.from(new Uint8Array(sig), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
15
16
|
}
|
|
17
|
+
/** The app's Garage bucket and S3 key, derived from WERVT_DB_SECRET like role passwords. */
|
|
18
|
+
async function storageCredentials(app, secret) {
|
|
19
|
+
return {
|
|
20
|
+
bucket: `app-${app}`,
|
|
21
|
+
accessKeyId: `GK${(await hmac(secret, `s3-id:${app}`)).slice(0, 24)}`,
|
|
22
|
+
secretAccessKey: await hmac(secret, `s3-secret:${app}`)
|
|
23
|
+
};
|
|
24
|
+
}
|
|
16
25
|
function dbPassword(app, secret) {
|
|
17
26
|
return hmac(secret, app);
|
|
18
27
|
}
|
|
@@ -120,4 +129,4 @@ async function dropDatabase({ app, adminUrl }) {
|
|
|
120
129
|
}
|
|
121
130
|
}
|
|
122
131
|
//#endregion
|
|
123
|
-
export { appDatabaseUrl, dbPassword, dbRole, dropDatabase, hmac, provisionDatabase };
|
|
132
|
+
export { StorageError, appDatabaseUrl, createStorage, dbPassword, dbRole, dropDatabase, hmac, provisionDatabase, storageCredentials };
|
|
@@ -2,6 +2,7 @@ import { WervtUser } from "../shared/user.js";
|
|
|
2
2
|
import { useDb } from "./db.js";
|
|
3
3
|
import { createEvlogError, log, useLogger } from "./logger.js";
|
|
4
4
|
import { ShapeOptions, proxyShape } from "./shape.js";
|
|
5
|
+
import { Task, TaskEvent, TaskResult, defineTask } from "./tasks.js";
|
|
5
6
|
import { useWervtUser } from "./user.js";
|
|
6
7
|
import { SafeResult, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, vh } from "./validate.js";
|
|
7
|
-
export { type SafeResult, type ShapeOptions, type WervtUser, createEvlogError, log, proxyShape, useDb, useLogger, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, useWervtUser, vh };
|
|
8
|
+
export { type SafeResult, type ShapeOptions, type Task, type TaskEvent, type TaskResult, type WervtUser, createEvlogError, defineTask, log, proxyShape, useDb, useLogger, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, useWervtUser, vh };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useDb } from "./db.js";
|
|
2
2
|
import { createEvlogError, log, useLogger } from "./logger.js";
|
|
3
3
|
import { proxyShape } from "./shape.js";
|
|
4
|
+
import { defineTask } from "./tasks.js";
|
|
4
5
|
import { useWervtUser } from "./user.js";
|
|
5
6
|
import { useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, vh } from "./validate.js";
|
|
6
|
-
export { createEvlogError, log, proxyShape, useDb, useLogger, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, useWervtUser, vh };
|
|
7
|
+
export { createEvlogError, defineTask, log, proxyShape, useDb, useLogger, useSafeValidatedBody, useSafeValidatedParams, useSafeValidatedQuery, useValidatedBody, useValidatedParams, useValidatedQuery, useWervtUser, vh };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createS3Provider, setFileStorageProvider } from "#imports";
|
|
2
|
+
//#region src/runtime/server/storage-plugin.ts
|
|
3
|
+
function wervtStorage() {
|
|
4
|
+
const env = process.env;
|
|
5
|
+
if (!env.WERVT_S3_URL || !env.WERVT_S3_BUCKET) return;
|
|
6
|
+
setFileStorageProvider(createS3Provider({
|
|
7
|
+
endpoint: env.WERVT_S3_URL,
|
|
8
|
+
region: env.WERVT_S3_REGION ?? "garage",
|
|
9
|
+
bucket: env.WERVT_S3_BUCKET,
|
|
10
|
+
accessKeyId: env.WERVT_S3_ACCESS_KEY_ID,
|
|
11
|
+
secretAccessKey: env.WERVT_S3_SECRET_ACCESS_KEY,
|
|
12
|
+
publicEndpoint: env.WERVT_S3_PUBLIC_URL
|
|
13
|
+
}));
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
export { wervtStorage as default };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
//#region src/runtime/server/task-handlers.d.ts
|
|
2
|
+
type RunTask = (name: string, options: {
|
|
3
|
+
payload?: Record<string, unknown>;
|
|
4
|
+
}) => Promise<{
|
|
5
|
+
result?: unknown;
|
|
6
|
+
}>;
|
|
7
|
+
type Registry = Record<string, {
|
|
8
|
+
meta?: {
|
|
9
|
+
description?: string;
|
|
10
|
+
};
|
|
11
|
+
resolve?: () => Promise<unknown>;
|
|
12
|
+
}>;
|
|
13
|
+
/** `POST /_wervt/tasks/:name`: runs a task when the control plane asks. */
|
|
14
|
+
export declare function taskRunHandler(runTask: RunTask): import("nuxt/server").EventHandler<Promise<{
|
|
15
|
+
error: string;
|
|
16
|
+
result?: undefined;
|
|
17
|
+
} | {
|
|
18
|
+
result: {} | null;
|
|
19
|
+
error?: undefined;
|
|
20
|
+
}>>;
|
|
21
|
+
/** `GET /_wervt/tasks`: the tasks with the descriptions from `defineTask({ meta })`. */
|
|
22
|
+
export declare function taskListHandler(tasks: Registry): import("nuxt/server").EventHandler<Promise<{
|
|
23
|
+
name: string;
|
|
24
|
+
description: string | undefined;
|
|
25
|
+
}[] | {
|
|
26
|
+
error: string;
|
|
27
|
+
}>>;
|
|
28
|
+
//#endregion
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { defineEventHandler } from "nuxt/server";
|
|
2
|
+
//#region src/runtime/server/task-handlers.ts
|
|
3
|
+
function trusted(event) {
|
|
4
|
+
if (event.req.headers.get("x-wervt-task") === "1") return true;
|
|
5
|
+
event.res.status = 404;
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
/** `POST /_wervt/tasks/:name`: runs a task when the control plane asks. */
|
|
9
|
+
function taskRunHandler(runTask) {
|
|
10
|
+
return defineEventHandler(async (event) => {
|
|
11
|
+
if (!trusted(event)) return { error: "not found" };
|
|
12
|
+
const name = decodeURIComponent(new URL(event.req.url).pathname.split("/").pop() ?? "");
|
|
13
|
+
const body = await event.req.json().catch(() => ({}));
|
|
14
|
+
event.res.headers.set("cache-control", "no-store");
|
|
15
|
+
try {
|
|
16
|
+
return { result: (await runTask(name, { payload: body.payload ?? {} })).result ?? null };
|
|
17
|
+
} catch (e) {
|
|
18
|
+
console.error(`Task ${name} failed:`, e);
|
|
19
|
+
event.res.status = e.status ?? 500;
|
|
20
|
+
return { error: e instanceof Error ? e.message : String(e) };
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/** `GET /_wervt/tasks`: the tasks with the descriptions from `defineTask({ meta })`. */
|
|
25
|
+
function taskListHandler(tasks) {
|
|
26
|
+
return defineEventHandler(async (event) => {
|
|
27
|
+
if (!trusted(event)) return { error: "not found" };
|
|
28
|
+
event.res.headers.set("cache-control", "no-store");
|
|
29
|
+
return Promise.all(Object.entries(tasks).map(async ([name, entry]) => {
|
|
30
|
+
return {
|
|
31
|
+
name,
|
|
32
|
+
description: (await entry.resolve?.().catch(() => void 0))?.meta?.description ?? entry.meta?.description
|
|
33
|
+
};
|
|
34
|
+
}));
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
export { taskListHandler, taskRunHandler };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { taskListHandler } from "./task-handlers.js";
|
|
2
|
+
import { tasks } from "#nitro-internal-virtual/tasks";
|
|
3
|
+
//#region src/runtime/server/task-list-nitro2.ts
|
|
4
|
+
var task_list_nitro2_default = taskListHandler(tasks);
|
|
5
|
+
//#endregion
|
|
6
|
+
export { task_list_nitro2_default as default };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { taskListHandler } from "./task-handlers.js";
|
|
2
|
+
import { tasks } from "#nitro/virtual/tasks";
|
|
3
|
+
//#region src/runtime/server/task-list-nitro3.ts
|
|
4
|
+
var task_list_nitro3_default = taskListHandler(tasks);
|
|
5
|
+
//#endregion
|
|
6
|
+
export { task_list_nitro3_default as default };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/runtime/server/task-run-nitro2.d.ts
|
|
2
|
+
declare const _default: import("nuxt/server").EventHandler<Promise<{
|
|
3
|
+
error: string;
|
|
4
|
+
result?: undefined;
|
|
5
|
+
} | {
|
|
6
|
+
result: {} | null;
|
|
7
|
+
error?: undefined;
|
|
8
|
+
}>>;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { _default as default };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { taskRunHandler } from "./task-handlers.js";
|
|
2
|
+
import { runTask } from "nitropack/runtime";
|
|
3
|
+
//#region src/runtime/server/task-run-nitro2.ts
|
|
4
|
+
var task_run_nitro2_default = taskRunHandler(runTask);
|
|
5
|
+
//#endregion
|
|
6
|
+
export { task_run_nitro2_default as default };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/runtime/server/task-run-nitro3.d.ts
|
|
2
|
+
declare const _default: import("nuxt/server").EventHandler<Promise<{
|
|
3
|
+
error: string;
|
|
4
|
+
result?: undefined;
|
|
5
|
+
} | {
|
|
6
|
+
result: {} | null;
|
|
7
|
+
error?: undefined;
|
|
8
|
+
}>>;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { _default as default };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { taskRunHandler } from "./task-handlers.js";
|
|
2
|
+
import { runTask } from "../../node_modules/.pnpm/nitro@3.0.260903-beta/node_modules/nitro/dist/runtime/internal/task.js";
|
|
3
|
+
import "../../node_modules/.pnpm/nitro@3.0.260903-beta/node_modules/nitro/dist/runtime/task.js";
|
|
4
|
+
//#region src/runtime/server/task-run-nitro3.ts
|
|
5
|
+
var task_run_nitro3_default = taskRunHandler(runTask);
|
|
6
|
+
//#endregion
|
|
7
|
+
export { task_run_nitro3_default as default };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//#region src/runtime/server/tasks.d.ts
|
|
2
|
+
export interface TaskEvent<P = Record<string, unknown>> {
|
|
3
|
+
name: string;
|
|
4
|
+
/** `{ scheduledTime }` for scheduled runs, or what was passed to a manual run. */
|
|
5
|
+
payload: P;
|
|
6
|
+
context: Record<string, unknown>;
|
|
7
|
+
}
|
|
8
|
+
export interface TaskResult<R = unknown> {
|
|
9
|
+
result?: R;
|
|
10
|
+
}
|
|
11
|
+
export interface Task<R = unknown, P = Record<string, unknown>> {
|
|
12
|
+
meta?: {
|
|
13
|
+
name?: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
};
|
|
16
|
+
run(event: TaskEvent<P>): TaskResult<R> | Promise<TaskResult<R>>;
|
|
17
|
+
}
|
|
18
|
+
/** A task in `server/tasks/` (`server/tasks/presence/sweep.ts` is `presence:sweep`). */
|
|
19
|
+
export declare function defineTask<R = unknown, P = Record<string, unknown>>(task: Task<R, P>): Task<R, P>;
|
|
20
|
+
//#endregion
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/runtime/server/tasks.ts
|
|
2
|
+
/** A task in `server/tasks/` (`server/tasks/presence/sweep.ts` is `presence:sweep`). */
|
|
3
|
+
function defineTask(task) {
|
|
4
|
+
if (typeof task.run !== "function") task.run = () => {
|
|
5
|
+
throw new TypeError("Task must implement a `run` method!");
|
|
6
|
+
};
|
|
7
|
+
return task;
|
|
8
|
+
}
|
|
9
|
+
//#endregion
|
|
10
|
+
export { defineTask };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//#region src/storage.d.ts
|
|
2
|
+
export interface StorageOptions {
|
|
3
|
+
/** Garage's admin API, e.g. http://garage:3903 */
|
|
4
|
+
adminUrl: string;
|
|
5
|
+
adminToken: string;
|
|
6
|
+
/** Garage's S3 API as the control plane reaches it, e.g. http://garage:3900 */
|
|
7
|
+
s3Url: string;
|
|
8
|
+
secret: string;
|
|
9
|
+
}
|
|
10
|
+
export interface StorageUsage {
|
|
11
|
+
bucket: string;
|
|
12
|
+
objects: number;
|
|
13
|
+
bytes: number;
|
|
14
|
+
/** Multipart uploads started but never completed or aborted. */
|
|
15
|
+
unfinishedUploads: number;
|
|
16
|
+
}
|
|
17
|
+
export declare class StorageError extends Error {}
|
|
18
|
+
export declare function createStorage(options: StorageOptions): {
|
|
19
|
+
ensureLayout: () => Promise<void>;
|
|
20
|
+
provision: (app: string, appOrigin: string) => Promise<{
|
|
21
|
+
bucket: string;
|
|
22
|
+
}>;
|
|
23
|
+
usage: (app: string) => Promise<StorageUsage | undefined>;
|
|
24
|
+
drop: (app: string) => Promise<void>;
|
|
25
|
+
};
|
|
26
|
+
//#endregion
|
package/dist/storage.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { storageCredentials } from "./provision.js";
|
|
2
|
+
import { AwsClient } from "aws4fetch";
|
|
3
|
+
//#region src/storage.ts
|
|
4
|
+
var StorageError = class extends Error {};
|
|
5
|
+
function createStorage(options) {
|
|
6
|
+
async function admin(path, body) {
|
|
7
|
+
const res = await fetch(`${options.adminUrl}/v2/${path}`, {
|
|
8
|
+
method: body === void 0 ? "GET" : "POST",
|
|
9
|
+
headers: {
|
|
10
|
+
authorization: `Bearer ${options.adminToken}`,
|
|
11
|
+
"content-type": "application/json"
|
|
12
|
+
},
|
|
13
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
14
|
+
});
|
|
15
|
+
if (!res.ok) throw new StorageError(`garage ${path}: ${res.status} ${await res.text()}`);
|
|
16
|
+
return await res.json();
|
|
17
|
+
}
|
|
18
|
+
async function exists(path) {
|
|
19
|
+
const res = await fetch(`${options.adminUrl}/v2/${path}`, { headers: { authorization: `Bearer ${options.adminToken}` } });
|
|
20
|
+
if (res.status === 404 || res.status === 400) return void 0;
|
|
21
|
+
if (!res.ok) throw new StorageError(`garage ${path}: ${res.status} ${await res.text()}`);
|
|
22
|
+
return await res.json();
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A fresh single-node Garage has no layout and stores nothing until it gets one. Gives this
|
|
26
|
+
* node all of its disk, once; later starts find the layout in place.
|
|
27
|
+
*/
|
|
28
|
+
async function ensureLayout() {
|
|
29
|
+
const node = (await admin("GetClusterStatus")).nodes[0];
|
|
30
|
+
if (!node || node.role) return;
|
|
31
|
+
await admin("UpdateClusterLayout", { roles: [{
|
|
32
|
+
id: node.id,
|
|
33
|
+
zone: "wervt",
|
|
34
|
+
capacity: node.dataPartition?.available ?? 0xe8d4a51000,
|
|
35
|
+
tags: []
|
|
36
|
+
}] });
|
|
37
|
+
await admin("ApplyClusterLayout", { version: (await admin("GetClusterLayout")).version + 1 });
|
|
38
|
+
}
|
|
39
|
+
const s3 = async (app) => {
|
|
40
|
+
const creds = await storageCredentials(app, options.secret);
|
|
41
|
+
return {
|
|
42
|
+
creds,
|
|
43
|
+
client: new AwsClient({
|
|
44
|
+
accessKeyId: creds.accessKeyId,
|
|
45
|
+
secretAccessKey: creds.secretAccessKey,
|
|
46
|
+
region: "garage",
|
|
47
|
+
service: "s3"
|
|
48
|
+
}),
|
|
49
|
+
base: `${options.s3Url}/${creds.bucket}`
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Creates the app's bucket and key if they're missing, lets the app's origin upload and
|
|
54
|
+
* download from browsers (CORS), and cleans up abandoned uploads. Safe to run on every deploy.
|
|
55
|
+
*/
|
|
56
|
+
async function provision(app, appOrigin) {
|
|
57
|
+
const { creds, client, base } = await s3(app);
|
|
58
|
+
const bucket = await exists(`GetBucketInfo?globalAlias=${creds.bucket}`) ?? await admin("CreateBucket", { globalAlias: creds.bucket });
|
|
59
|
+
if (!await exists(`GetKeyInfo?id=${creds.accessKeyId}`)) await admin("ImportKey", {
|
|
60
|
+
accessKeyId: creds.accessKeyId,
|
|
61
|
+
secretAccessKey: creds.secretAccessKey,
|
|
62
|
+
name: creds.bucket
|
|
63
|
+
});
|
|
64
|
+
await admin("AllowBucketKey", {
|
|
65
|
+
bucketId: bucket.id,
|
|
66
|
+
accessKeyId: creds.accessKeyId,
|
|
67
|
+
permissions: {
|
|
68
|
+
read: true,
|
|
69
|
+
write: true,
|
|
70
|
+
owner: true
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
const cors = `<CORSConfiguration><CORSRule>
|
|
74
|
+
<AllowedOrigin>${appOrigin}</AllowedOrigin>
|
|
75
|
+
<AllowedMethod>GET</AllowedMethod><AllowedMethod>HEAD</AllowedMethod>
|
|
76
|
+
<AllowedMethod>PUT</AllowedMethod><AllowedMethod>POST</AllowedMethod>
|
|
77
|
+
<AllowedHeader>*</AllowedHeader><ExposeHeader>ETag</ExposeHeader>
|
|
78
|
+
<MaxAgeSeconds>3600</MaxAgeSeconds>
|
|
79
|
+
</CORSRule></CORSConfiguration>`;
|
|
80
|
+
const res = await client.fetch(`${base}?cors`, {
|
|
81
|
+
method: "PUT",
|
|
82
|
+
body: cors
|
|
83
|
+
});
|
|
84
|
+
if (!res.ok) throw new StorageError(`setting CORS for ${creds.bucket}: ${await res.text()}`);
|
|
85
|
+
const rules = await client.fetch(`${base}?lifecycle`, {
|
|
86
|
+
method: "PUT",
|
|
87
|
+
body: `<LifecycleConfiguration><Rule>
|
|
88
|
+
<ID>abort-incomplete-uploads</ID><Status>Enabled</Status><Filter></Filter>
|
|
89
|
+
<AbortIncompleteMultipartUpload><DaysAfterInitiation>1</DaysAfterInitiation></AbortIncompleteMultipartUpload>
|
|
90
|
+
</Rule></LifecycleConfiguration>`
|
|
91
|
+
});
|
|
92
|
+
if (!rules.ok) throw new StorageError(`setting the lifecycle for ${creds.bucket}: ${await rules.text()}`);
|
|
93
|
+
return { bucket: creds.bucket };
|
|
94
|
+
}
|
|
95
|
+
async function usage(app) {
|
|
96
|
+
const { creds } = await s3(app);
|
|
97
|
+
const info = await exists(`GetBucketInfo?globalAlias=${creds.bucket}`);
|
|
98
|
+
if (!info) return void 0;
|
|
99
|
+
return {
|
|
100
|
+
bucket: creds.bucket,
|
|
101
|
+
objects: info.objects,
|
|
102
|
+
bytes: info.bytes,
|
|
103
|
+
unfinishedUploads: info.unfinishedMultipartUploads
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** Deletes every object and unfinished upload, then the bucket. Only with "drop data". */
|
|
107
|
+
async function drop(app) {
|
|
108
|
+
const { creds, client, base } = await s3(app);
|
|
109
|
+
const bucket = await exists(`GetBucketInfo?globalAlias=${creds.bucket}`);
|
|
110
|
+
if (bucket) {
|
|
111
|
+
for (;;) {
|
|
112
|
+
const keys = [...(await (await client.fetch(`${base}?list-type=2&max-keys=1000`)).text()).matchAll(/<Key>([^<]*)<\/Key>/g)].map((m) => m[1]);
|
|
113
|
+
if (!keys.length) break;
|
|
114
|
+
const body = `<Delete><Quiet>true</Quiet>${keys.map((key) => `<Object><Key>${key}</Key></Object>`).join("")}</Delete>`;
|
|
115
|
+
const res = await client.fetch(`${base}?delete`, {
|
|
116
|
+
method: "POST",
|
|
117
|
+
body
|
|
118
|
+
});
|
|
119
|
+
if (!res.ok) throw new StorageError(`emptying ${creds.bucket}: ${await res.text()}`);
|
|
120
|
+
}
|
|
121
|
+
for (;;) {
|
|
122
|
+
const uploads = [...(await (await client.fetch(`${base}?uploads&max-uploads=1000`)).text()).matchAll(/<Key>([^<]*)<\/Key>\s*<UploadId>([^<]*)<\/UploadId>/g)].map((m) => ({
|
|
123
|
+
key: m[1],
|
|
124
|
+
id: m[2]
|
|
125
|
+
}));
|
|
126
|
+
if (!uploads.length) break;
|
|
127
|
+
for (const upload of uploads) {
|
|
128
|
+
const url = `${base}/${upload.key.replaceAll("&", "&").split("/").map(encodeURIComponent).join("/")}?uploadId=${encodeURIComponent(upload.id)}`;
|
|
129
|
+
const res = await client.fetch(url, { method: "DELETE" });
|
|
130
|
+
if (!res.ok && res.status !== 404) throw new StorageError(`aborting an upload in ${creds.bucket}: ${await res.text()}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
await admin(`DeleteBucket?id=${String(bucket.id)}`, {});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
ensureLayout,
|
|
138
|
+
provision,
|
|
139
|
+
usage,
|
|
140
|
+
drop
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
//#endregion
|
|
144
|
+
export { StorageError, createStorage };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wervt/nuxt",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Nuxt module for apps hosted on wervt: database, live data and runtime setup.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"drizzle",
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@electric-sql/client": "^1.5.28",
|
|
42
|
+
"aws4fetch": "^1.0.20",
|
|
42
43
|
"evlog": "^2.30.0",
|
|
43
44
|
"postgres": "^3.4.9",
|
|
44
45
|
"valibot": "^1.5.0"
|