pocketbomb-detonator 0.2.0 → 0.3.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/README.md CHANGED
@@ -30,6 +30,8 @@ export const { authenticate, unauthenticated } = app;
30
30
  token, reload with `id_token`, stripped with `history.replaceState`); outside of an iframe a page links back to the dashboard.
31
31
  - `addDocumentResponseHeaders(request, headers)`: `Content-Security-Policy: frame-ancestors {dashboard origin}` and
32
32
  `Referrer-Policy: no-referrer`; `handleEmbeddedRequest(request, {paths})`: the launches + the token checks before the router.
33
+ - `withoutIdToken(request)`: after `handleEmbeddedRequest()` and before an SSR router: moves the `id_token` of the bounce to the
34
+ `Authorization` header, so that the token doesn't end up in the rendered HTML (e.g. the form actions).
33
35
  - `authenticate.public.appProxy(request)`: a request forwarded by the app proxy -> verifies the signature over the
34
36
  raw query (Shopify algorithm with the webhook secret), `body_sha256` and `timestamp` (±90s) ->
35
37
  `{installation, customerId, pathPrefix, admin}`.
@@ -44,7 +46,10 @@ path; any other path falls through to the `GET /{path...}` static route if there
44
46
  without being more specific or more general (e.g. `/{a}/foo/bar`) makes the server panic at startup (intentionally loud).
45
47
 
46
48
  Entries: `.` (verifyApplicationJWT, verifyWebhookSignature, createAdminClient, exchangeLaunchToken),
47
- `./server`, `./react` (AppProvider/useApp), `./adapters/node`, `./session-storage` (Memory, `node:sqlite`).
49
+ `./server` (also `scopesToPermissions`), `./react` (AppProvider/useApp), `./adapters/node`, `./session-storage` (Memory, `node:sqlite`).
48
50
 
49
- CLI: `detonator dev` (fuse -> import `pocketbomb.app.toml` -> run the app -> install with the manifest scopes),
50
- `detonator deploy --url --token`, `detonator info`.
51
+ CLI: `detonator dev` (fuse -> import `pocketbomb.app.toml` -> run the app -> install with the manifest scopes; `--data-dir` and
52
+ `--fuse-port` for fuse, or an existing server with `--url` + `--token` / `$POCKETBOMB_URL` + `$POCKETBOMB_TOKEN`: dev versions
53
+ derived from the manifest content, an existing installation is reused with new credentials, and the server keeps running),
54
+ `detonator deploy --url --token`, `detonator install --url --token [--config] [--dotenv]` (the manifest scopes; idempotent;
55
+ without an `install_url` the credentials go to the env file, never the output), `detonator info`.
@@ -1,24 +1,36 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
3
+ const require_scopes = require("../scopes.cjs");
3
4
  const require_manifest = require("./manifest.cjs");
5
+ let pocketbomb_primer = require("pocketbomb-primer");
4
6
  let node_child_process = require("node:child_process");
7
+ let node_crypto = require("node:crypto");
5
8
  let node_fs = require("node:fs");
6
9
  node_fs = require_runtime.__toESM(node_fs, 1);
7
10
  let node_path = require("node:path");
8
11
  node_path = require_runtime.__toESM(node_path, 1);
9
12
  let node_util = require("node:util");
13
+ let pocketbomb_primer_internal = require("pocketbomb-primer/internal");
10
14
  // src/cli/index.ts
11
15
  const HELP = `detonator <command> [options]
12
16
 
13
17
  Commands:
14
- dev Start fuse, import pocketbomb.app.toml as a custom app, run the app and install it
18
+ dev Start fuse (unless --url/$POCKETBOMB_URL is set), import pocketbomb.app.toml as a custom app,
19
+ run the app and install it
15
20
  --config <file> (default pocketbomb.app.toml)
21
+ --url <url> --token <staff access token>
22
+ use an existing server instead of fuse (or $POCKETBOMB_URL, $POCKETBOMB_TOKEN)
16
23
  --seed <module> the fuse seed (e.g. the collections of the scopes)
24
+ --data-dir <dir> a persistent fuse data dir (default a temporary one)
25
+ --fuse-port <port> the fuse server port (default a free port)
17
26
  --entry <file> the app server entry run with Node.js (default server.ts)
18
27
  --port <port> the app port (default a free port)
19
28
  --dotenv <file> the env file to update (default .env)
20
29
  deploy Deploy (import) the manifest to a PocketBomb instance
21
30
  --url <url> --token <staff access token> [--config <file>] [--app-url <url>]
31
+ install Install the deployed application with the scopes of the manifest (idempotent)
32
+ --url <url> --token <staff access token> [--config <file>]
33
+ --dotenv <file> where the credentials go without an install_url (default .env)
22
34
  info Print the manifest summary and the environment
23
35
  `;
24
36
  const defaultIo = {
@@ -43,6 +55,22 @@ async function waitForApp(url, child, timeout = 6e4) {
43
55
  }
44
56
  throw new Error("The app didn't start in " + timeout + "ms.");
45
57
  }
58
+ function staffClient(url, token) {
59
+ const client = new pocketbomb_primer_internal.DashboardClient(url, { authStore: new pocketbomb_primer.AuthStore() });
60
+ client.authStore.save(token.replace(/^Bearer /, ""), null);
61
+ return client;
62
+ }
63
+ function devVersion(manifest) {
64
+ const digest = (0, node_crypto.createHash)("sha256").update(JSON.stringify(manifest)).digest("hex").substring(0, 12);
65
+ return (manifest.version || "0.0.0").split("-")[0] + "-dev." + digest;
66
+ }
67
+ function isLoopback(url) {
68
+ return [
69
+ "localhost",
70
+ "127.0.0.1",
71
+ "[::1]"
72
+ ].includes(new URL(url).hostname);
73
+ }
46
74
  /**
47
75
  * `detonator dev`: fuse -> import the manifest (with the dev app URL) as a custom application ->
48
76
  * run the app -> install it with the manifest scopes (the credentials are delivered to the running
@@ -55,14 +83,20 @@ async function dev(options = {}) {
55
83
  const envFile = node_path.default.resolve(cwd, options.envFile || ".env");
56
84
  const entry = node_path.default.resolve(cwd, options.entry || "server.ts");
57
85
  const { freePort, signInStaff, startFuse } = await import("pocketbomb-fuse/node");
86
+ const serverUrl = (options.url || process.env.POCKETBOMB_URL || "").replace(/\/+$/, "");
87
+ const token = options.token || process.env.POCKETBOMB_TOKEN;
88
+ if (serverUrl && !token) throw new Error("detonator dev: --token (or $POCKETBOMB_TOKEN) is required with an existing server.");
58
89
  const port = options.port || await freePort("localhost");
59
90
  const appUrl = "http://localhost:" + port;
60
91
  const manifest = require_manifest.resolveManifest(require_manifest.loadManifest(manifestFile), appUrl);
61
- const fuse = await startFuse({
92
+ const fuse = serverUrl ? null : await startFuse({
62
93
  seed: options.seed ? node_path.default.resolve(cwd, options.seed) : void 0,
94
+ dataDir: options.dataDir ? node_path.default.resolve(cwd, options.dataDir) : void 0,
95
+ port: options.fusePort,
63
96
  log: (line) => io.error("[fuse] " + line)
64
97
  });
65
- io.error("[detonator] PocketBomb (fuse) running on " + fuse.url);
98
+ const pocketbombUrl = fuse ? fuse.url : serverUrl;
99
+ io.error(fuse ? "[detonator] PocketBomb (fuse) running on " + fuse.url : "[detonator] using the PocketBomb server " + serverUrl);
66
100
  let app = null;
67
101
  const stop = async () => {
68
102
  if (app && app.exitCode === null) {
@@ -70,22 +104,24 @@ async function dev(options = {}) {
70
104
  app.kill("SIGTERM");
71
105
  await exited;
72
106
  }
73
- await fuse.stop();
107
+ await fuse?.stop();
74
108
  };
75
109
  try {
76
- const owner = await signInStaff(fuse.url, fuse.owner.email, fuse.owner.password);
77
- await owner.settings.update({
110
+ const owner = fuse ? await signInStaff(fuse.url, fuse.owner.email, fuse.owner.password) : staffClient(serverUrl, token);
111
+ if (fuse) await owner.settings.update({
78
112
  meta: { appURL: fuse.url },
79
113
  webhooks: { allowPrivateTargets: true }
80
114
  });
115
+ else if (isLoopback(serverUrl)) await owner.settings.update({ webhooks: { allowPrivateTargets: true } });
116
+ if (!fuse) manifest.version = devVersion(manifest);
81
117
  const imported = await owner.applications.importManifest(manifest, "json");
82
118
  io.error("[detonator] imported " + manifest.handle + " " + imported.version.version);
83
119
  const collections = await owner.collections.getFullList({ fields: "id,name" });
84
- const permissions = require_manifest.scopesToPermissions(manifest.access?.scopes || [], collections);
85
- const issuer = fuse.url + "/oauth2/_applicationInstallations";
120
+ const permissions = require_scopes.scopesToPermissions(manifest.access?.scopes || [], collections);
121
+ const issuer = pocketbombUrl + "/oauth2/_applicationInstallations";
86
122
  const baseEnv = {
87
- ...fuse.env,
88
- POCKETBOMB_URL: fuse.url,
123
+ ...fuse?.env,
124
+ POCKETBOMB_URL: pocketbombUrl,
89
125
  POCKETBOMB_ISSUER: issuer,
90
126
  POCKETBOMB_APP_HANDLE: manifest.handle,
91
127
  APP_URL: appUrl,
@@ -104,17 +140,26 @@ async function dev(options = {}) {
104
140
  await waitForApp(appUrl + "/", app);
105
141
  io.error("[detonator] app running on " + appUrl);
106
142
  };
143
+ const existing = fuse ? null : (await owner.applications.getList()).items.find((a) => a.id === imported.application.id)?.installation;
144
+ const installApp = async () => {
145
+ if (!existing) return owner.applications.install(imported.application.id, permissions);
146
+ await owner.applications.update(existing.id, {
147
+ permissions,
148
+ versionRef: imported.version.id
149
+ });
150
+ return owner.applications.rotateSecret(existing.id);
151
+ };
107
152
  let installed;
108
153
  let envValues;
109
154
  if (manifest.install_url) {
110
155
  await startApp(baseEnv);
111
- installed = await owner.applications.install(imported.application.id, permissions);
156
+ installed = await installApp();
112
157
  envValues = {
113
158
  ...baseEnv,
114
159
  POCKETBOMB_INSTALLATION_ID: installed.installation.id
115
160
  };
116
161
  } else {
117
- installed = await owner.applications.install(imported.application.id, permissions);
162
+ installed = await installApp();
118
163
  envValues = {
119
164
  ...baseEnv,
120
165
  POCKETBOMB_INSTALLATION_ID: installed.installation.id,
@@ -127,14 +172,14 @@ async function dev(options = {}) {
127
172
  require_manifest.updateEnvFile(envFile, envValues);
128
173
  io.error("[detonator] installed " + manifest.handle + " (installation " + installed.installation.id + (installed.notified ? ", credentials delivered to the install_url" : ", credentials in " + node_path.default.basename(envFile)) + ")");
129
174
  const state = {
130
- pocketbombUrl: fuse.url,
175
+ pocketbombUrl,
131
176
  appUrl,
132
177
  issuer,
133
178
  installationId: installed.installation.id,
134
179
  applicationId: imported.application.id,
135
180
  notified: installed.notified,
136
181
  app,
137
- owner: fuse.owner,
182
+ owner: fuse?.owner ?? null,
138
183
  stop
139
184
  };
140
185
  await options.onReady?.(state);
@@ -149,7 +194,11 @@ async function runDev(argv, io) {
149
194
  args: argv,
150
195
  options: {
151
196
  config: { type: "string" },
197
+ url: { type: "string" },
198
+ token: { type: "string" },
152
199
  seed: { type: "string" },
200
+ "data-dir": { type: "string" },
201
+ "fuse-port": { type: "string" },
153
202
  entry: { type: "string" },
154
203
  port: { type: "string" },
155
204
  dotenv: { type: "string" }
@@ -157,13 +206,17 @@ async function runDev(argv, io) {
157
206
  });
158
207
  const state = await dev({
159
208
  config: values.config,
209
+ url: values.url,
210
+ token: values.token,
160
211
  seed: values.seed,
212
+ dataDir: values["data-dir"],
213
+ fusePort: values["fuse-port"] ? parseInt(values["fuse-port"], 10) : void 0,
161
214
  entry: values.entry,
162
215
  port: values.port ? parseInt(values.port, 10) : void 0,
163
216
  envFile: values.dotenv,
164
217
  io
165
218
  });
166
- io.log("Dashboard: " + state.pocketbombUrl + "/_/ (" + state.owner.email + " / " + state.owner.password + ")");
219
+ io.log("Dashboard: " + state.pocketbombUrl + "/_/" + (state.owner ? " (" + state.owner.email + " / " + state.owner.password + ")" : ""));
167
220
  io.log("App: " + state.appUrl);
168
221
  const code = await new Promise((resolve) => {
169
222
  state.app.once("exit", (c) => resolve(c ?? 0));
@@ -199,6 +252,51 @@ async function runDeploy(argv, io) {
199
252
  io.log((result.created ? "created " : result.unchanged ? "unchanged " : "new version of ") + manifest.handle + " " + (result.version?.version || "") + " (application " + result.application?.id + ")");
200
253
  return 0;
201
254
  }
255
+ async function runInstall(argv, io) {
256
+ const { values } = (0, node_util.parseArgs)({
257
+ args: argv,
258
+ options: {
259
+ url: { type: "string" },
260
+ token: { type: "string" },
261
+ config: { type: "string" },
262
+ dotenv: { type: "string" }
263
+ }
264
+ });
265
+ const url = values.url || process.env.POCKETBOMB_URL;
266
+ const token = values.token || process.env.POCKETBOMB_TOKEN;
267
+ if (!url || !token) {
268
+ io.error("detonator install: --url and --token are required (or $POCKETBOMB_URL / $POCKETBOMB_TOKEN)");
269
+ return 2;
270
+ }
271
+ const manifest = require_manifest.loadManifest(node_path.default.resolve(values.config || "pocketbomb.app.toml"));
272
+ const owner = staffClient(url.replace(/\/+$/, ""), token);
273
+ const application = (await owner.applications.getList()).items.find((a) => a.handle === manifest.handle);
274
+ if (!application) {
275
+ io.error("detonator install: " + manifest.handle + " isn't deployed (run detonator deploy).");
276
+ return 1;
277
+ }
278
+ if (application.installation) {
279
+ io.log(manifest.handle + " is already installed (installation " + application.installation.id + ")");
280
+ return 0;
281
+ }
282
+ const collections = await owner.collections.getFullList({ fields: "id,name" });
283
+ const permissions = require_scopes.scopesToPermissions(manifest.access?.scopes || [], collections);
284
+ const installed = await owner.applications.install(application.id, permissions);
285
+ if (installed.notified) {
286
+ io.log("installed " + manifest.handle + " (installation " + installed.installation.id + ", credentials delivered to the install_url)");
287
+ return 0;
288
+ }
289
+ const envFile = node_path.default.resolve(values.dotenv || ".env");
290
+ require_manifest.updateEnvFile(envFile, {
291
+ POCKETBOMB_URL: url.replace(/\/+$/, ""),
292
+ POCKETBOMB_INSTALLATION_ID: installed.installation.id,
293
+ POCKETBOMB_CLIENT_ID: installed.clientId,
294
+ POCKETBOMB_CLIENT_SECRET: installed.clientSecret,
295
+ POCKETBOMB_WEBHOOK_SECRET: installed.webhookSecret
296
+ });
297
+ io.log("installed " + manifest.handle + " (installation " + installed.installation.id + ", credentials in " + node_path.default.basename(envFile) + ")");
298
+ return 0;
299
+ }
202
300
  function runInfo(argv, io) {
203
301
  const { values } = (0, node_util.parseArgs)({
204
302
  args: argv,
@@ -232,6 +330,7 @@ async function run(argv, io = defaultIo) {
232
330
  switch (command) {
233
331
  case "dev": return runDev(rest, io);
234
332
  case "deploy": return runDeploy(rest, io);
333
+ case "install": return runInstall(rest, io);
235
334
  case "info": return runInfo(rest, io);
236
335
  case "--version":
237
336
  case "-v":
@@ -254,7 +353,7 @@ exports.formatEnv = require_manifest.formatEnv;
254
353
  exports.loadManifest = require_manifest.loadManifest;
255
354
  exports.resolveManifest = require_manifest.resolveManifest;
256
355
  exports.run = run;
257
- exports.scopesToPermissions = require_manifest.scopesToPermissions;
356
+ exports.scopesToPermissions = require_scopes.scopesToPermissions;
258
357
  exports.updateEnvFile = require_manifest.updateEnvFile;
259
358
 
260
359
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["fs","path","resolveManifest","loadManifest","scopesToPermissions","spawn","parseArgs","deployManifest"],"sources":["../../src/cli/index.ts"],"sourcesContent":["// The `detonator` CLI (Node.js only): dev, deploy, info.\nimport { spawn, type ChildProcess } from \"node:child_process\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { parseArgs } from \"node:util\";\nimport {\n deployManifest,\n loadManifest,\n resolveManifest,\n scopesToPermissions,\n updateEnvFile,\n type AppManifest,\n} from \"./manifest\";\n\nexport * from \"./manifest\";\n\nconst HELP = `detonator <command> [options]\n\nCommands:\n dev Start fuse, import pocketbomb.app.toml as a custom app, run the app and install it\n --config <file> (default pocketbomb.app.toml)\n --seed <module> the fuse seed (e.g. the collections of the scopes)\n --entry <file> the app server entry run with Node.js (default server.ts)\n --port <port> the app port (default a free port)\n --dotenv <file> the env file to update (default .env)\n deploy Deploy (import) the manifest to a PocketBomb instance\n --url <url> --token <staff access token> [--config <file>] [--app-url <url>]\n info Print the manifest summary and the environment\n`;\n\ntype Io = { log: (line: string) => void; error: (line: string) => void };\n\nconst defaultIo: Io = {\n log: (line) => process.stdout.write(line + \"\\n\"),\n error: (line) => process.stderr.write(line + \"\\n\"),\n};\n\nfunction readPackageVersion(): string {\n try {\n return JSON.parse(fs.readFileSync(new URL(\"../../package.json\", import.meta.url), \"utf8\"))\n .version;\n } catch {\n return \"unknown\";\n }\n}\n\nasync function waitForApp(url: string, child: ChildProcess, timeout = 60000): Promise<void> {\n const deadline = Date.now() + timeout;\n while (Date.now() < deadline) {\n if (child.exitCode !== null) {\n throw new Error(\"The app exited early (code \" + child.exitCode + \").\");\n }\n try {\n const res = await fetch(url, { redirect: \"manual\" });\n if (res.status < 500) {\n return;\n }\n } catch {\n // not listening yet\n }\n // poll interval of the app startup readiness check\n await new Promise((r) => setTimeout(r, 150));\n }\n throw new Error(\"The app didn't start in \" + timeout + \"ms.\");\n}\n\nexport interface DevOptions {\n cwd?: string;\n config?: string;\n seed?: string;\n entry?: string;\n port?: number;\n envFile?: string;\n io?: Io;\n /** Called once the app is installed (the CLI waits for the app process otherwise). */\n onReady?: (state: DevState) => void | Promise<void>;\n}\n\nexport interface DevState {\n pocketbombUrl: string;\n appUrl: string;\n issuer: string;\n installationId: string;\n applicationId: string;\n notified: boolean;\n app: ChildProcess;\n owner: { email: string; password: string };\n stop(): Promise<void>;\n}\n\n/**\n * `detonator dev`: fuse -> import the manifest (with the dev app URL) as a custom application ->\n * run the app -> install it with the manifest scopes (the credentials are delivered to the running\n * app install_url, or written to the env file for a manifest without install_url).\n */\nexport async function dev(options: DevOptions = {}): Promise<DevState> {\n const io = options.io || defaultIo;\n const cwd = path.resolve(options.cwd || process.cwd());\n const manifestFile = path.resolve(cwd, options.config || \"pocketbomb.app.toml\");\n const envFile = path.resolve(cwd, options.envFile || \".env\");\n const entry = path.resolve(cwd, options.entry || \"server.ts\");\n\n const { freePort, signInStaff, startFuse } = await import(\"pocketbomb-fuse/node\");\n\n const port = options.port || (await freePort(\"localhost\"));\n const appUrl = \"http://localhost:\" + port;\n const manifest = resolveManifest(loadManifest(manifestFile), appUrl);\n\n const fuse = await startFuse({\n seed: options.seed ? path.resolve(cwd, options.seed) : undefined,\n log: (line) => io.error(\"[fuse] \" + line),\n });\n io.error(\"[detonator] PocketBomb (fuse) running on \" + fuse.url);\n\n let app: ChildProcess | null = null;\n const stop = async () => {\n if (app && app.exitCode === null) {\n const exited = new Promise((r) => app!.once(\"exit\", r));\n app.kill(\"SIGTERM\");\n await exited;\n }\n await fuse.stop();\n };\n\n try {\n const owner = await signInStaff(fuse.url, fuse.owner.email, fuse.owner.password);\n // the dev app (and its webhooks) are on a loopback host\n await owner.settings.update({\n meta: { appURL: fuse.url },\n webhooks: { allowPrivateTargets: true },\n });\n\n const imported = await owner.applications.importManifest(manifest as any, \"json\");\n io.error(\"[detonator] imported \" + manifest.handle + \" \" + imported.version.version);\n\n const collections = await owner.collections.getFullList({ fields: \"id,name\" });\n const permissions = scopesToPermissions(manifest.access?.scopes || [], collections);\n const issuer = fuse.url + \"/oauth2/_applicationInstallations\";\n\n const baseEnv: Record<string, string> = {\n ...fuse.env,\n POCKETBOMB_URL: fuse.url,\n POCKETBOMB_ISSUER: issuer,\n POCKETBOMB_APP_HANDLE: manifest.handle,\n APP_URL: appUrl,\n PORT: String(port),\n };\n\n const startApp = async (env: Record<string, string>) => {\n app = spawn(process.execPath, [entry], {\n cwd,\n stdio: \"inherit\",\n env: { ...process.env, ...env, NODE_ENV: process.env.NODE_ENV || \"development\" },\n });\n await waitForApp(appUrl + \"/\", app);\n io.error(\"[detonator] app running on \" + appUrl);\n };\n\n let installed;\n let envValues: Record<string, string>;\n if (manifest.install_url) {\n // the credentials are delivered to the running app (install notification)\n await startApp(baseEnv);\n installed = await owner.applications.install(imported.application.id, permissions as any);\n envValues = { ...baseEnv, POCKETBOMB_INSTALLATION_ID: installed.installation.id };\n } else {\n // a custom app without install_url gets the credentials once: env file + app env\n installed = await owner.applications.install(imported.application.id, permissions as any);\n envValues = {\n ...baseEnv,\n POCKETBOMB_INSTALLATION_ID: installed.installation.id,\n POCKETBOMB_CLIENT_ID: installed.clientId,\n POCKETBOMB_CLIENT_SECRET: installed.clientSecret,\n POCKETBOMB_WEBHOOK_SECRET: installed.webhookSecret,\n };\n await startApp(envValues);\n }\n\n updateEnvFile(envFile, envValues);\n io.error(\n \"[detonator] installed \" +\n manifest.handle +\n \" (installation \" +\n installed.installation.id +\n (installed.notified\n ? \", credentials delivered to the install_url\"\n : \", credentials in \" + path.basename(envFile)) +\n \")\",\n );\n\n const state: DevState = {\n pocketbombUrl: fuse.url,\n appUrl,\n issuer,\n installationId: installed.installation.id,\n applicationId: imported.application.id,\n notified: installed.notified,\n app: app!,\n owner: fuse.owner,\n stop,\n };\n\n await options.onReady?.(state);\n\n return state;\n } catch (err) {\n await stop();\n throw err;\n }\n}\n\nasync function runDev(argv: Array<string>, io: Io): Promise<number> {\n const { values } = parseArgs({\n args: argv,\n options: {\n config: { type: \"string\" },\n seed: { type: \"string\" },\n entry: { type: \"string\" },\n port: { type: \"string\" },\n dotenv: { type: \"string\" },\n },\n });\n\n const state = await dev({\n config: values.config,\n seed: values.seed,\n entry: values.entry,\n port: values.port ? parseInt(values.port, 10) : undefined,\n envFile: values.dotenv,\n io,\n });\n io.log(\n \"Dashboard: \" +\n state.pocketbombUrl +\n \"/_/ (\" +\n state.owner.email +\n \" / \" +\n state.owner.password +\n \")\",\n );\n io.log(\"App: \" + state.appUrl);\n\n const code: number = await new Promise((resolve) => {\n state.app.once(\"exit\", (c) => resolve(c ?? 0));\n const shutdown = () => void state.stop().then(() => resolve(0));\n process.once(\"SIGINT\", shutdown);\n process.once(\"SIGTERM\", shutdown);\n });\n await state.stop();\n return code;\n}\n\nasync function runDeploy(argv: Array<string>, io: Io): Promise<number> {\n const { values } = parseArgs({\n args: argv,\n options: {\n url: { type: \"string\" },\n token: { type: \"string\" },\n config: { type: \"string\" },\n \"app-url\": { type: \"string\" },\n },\n });\n\n const url = values.url || process.env.POCKETBOMB_URL;\n const token = values.token || process.env.POCKETBOMB_TOKEN;\n if (!url || !token) {\n io.error(\n \"detonator deploy: --url and --token are required (or $POCKETBOMB_URL / $POCKETBOMB_TOKEN)\",\n );\n return 2;\n }\n\n const manifest = resolveManifest(\n loadManifest(path.resolve(values.config || \"pocketbomb.app.toml\")),\n values[\"app-url\"],\n );\n\n const result = await deployManifest({ url, token, manifest });\n io.log(\n (result.created ? \"created \" : result.unchanged ? \"unchanged \" : \"new version of \") +\n manifest.handle +\n \" \" +\n (result.version?.version || \"\") +\n \" (application \" +\n result.application?.id +\n \")\",\n );\n return 0;\n}\n\nfunction runInfo(argv: Array<string>, io: Io): number {\n const { values } = parseArgs({ args: argv, options: { config: { type: \"string\" } } });\n const file = path.resolve(values.config || \"pocketbomb.app.toml\");\n\n io.log(\"pocketbomb-detonator \" + readPackageVersion());\n io.log(\"node \" + process.version);\n\n let manifest: AppManifest;\n try {\n manifest = loadManifest(file);\n } catch (err) {\n io.log(\"manifest \" + String((err as Error).message));\n return 1;\n }\n\n io.log(\"manifest \" + path.basename(file));\n io.log(\"handle \" + manifest.handle + \" \" + (manifest.version || \"\"));\n io.log(\"application_url \" + (manifest.application_url || \"(not set)\"));\n io.log(\"scopes \" + (manifest.access?.scopes || []).join(\", \"));\n io.log(\n \"webhooks \" +\n (manifest.webhooks?.subscriptions || [])\n .map((s) => s.topics.join(\",\") + \" -> \" + s.uri)\n .join(\"; \"),\n );\n io.log(\n \"extensions \" +\n (manifest.extensions || []).map((e) => e.type + \":\" + e.handle + \" \" + e.path).join(\"; \"),\n );\n io.log(\"POCKETBOMB_URL \" + (process.env.POCKETBOMB_URL || \"(not set)\"));\n io.log(\n \"POCKETBOMB_CLIENT_SECRET \" + (process.env.POCKETBOMB_CLIENT_SECRET ? \"(set)\" : \"(not set)\"),\n );\n return 0;\n}\n\n/**\n * Runs the CLI with the provided arguments (without the node and script paths).\n */\nexport async function run(argv: Array<string>, io: Io = defaultIo): Promise<number> {\n const [command, ...rest] = argv;\n\n switch (command) {\n case \"dev\":\n return runDev(rest, io);\n case \"deploy\":\n return runDeploy(rest, io);\n case \"info\":\n return runInfo(rest, io);\n case \"--version\":\n case \"-v\":\n io.log(readPackageVersion());\n return 0;\n case undefined:\n case \"help\":\n case \"--help\":\n case \"-h\":\n io.log(HELP);\n return 0;\n default:\n io.error(\"Unknown command: \" + command + \"\\n\\n\" + HELP);\n return 2;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["fs","DashboardClient","AuthStore","createHash","path","resolveManifest","loadManifest","scopesToPermissions","spawn","parseArgs","deployManifest"],"sources":["../../src/cli/index.ts"],"sourcesContent":["// The `detonator` CLI (Node.js only): dev, deploy, info.\nimport { spawn, type ChildProcess } from \"node:child_process\";\nimport { createHash } from \"node:crypto\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { parseArgs } from \"node:util\";\nimport { AuthStore } from \"pocketbomb-primer\";\nimport { DashboardClient } from \"pocketbomb-primer/internal\";\nimport {\n deployManifest,\n loadManifest,\n resolveManifest,\n scopesToPermissions,\n updateEnvFile,\n type AppManifest,\n} from \"./manifest\";\n\nexport * from \"./manifest\";\n\nconst HELP = `detonator <command> [options]\n\nCommands:\n dev Start fuse (unless --url/$POCKETBOMB_URL is set), import pocketbomb.app.toml as a custom app,\n run the app and install it\n --config <file> (default pocketbomb.app.toml)\n --url <url> --token <staff access token>\n use an existing server instead of fuse (or $POCKETBOMB_URL, $POCKETBOMB_TOKEN)\n --seed <module> the fuse seed (e.g. the collections of the scopes)\n --data-dir <dir> a persistent fuse data dir (default a temporary one)\n --fuse-port <port> the fuse server port (default a free port)\n --entry <file> the app server entry run with Node.js (default server.ts)\n --port <port> the app port (default a free port)\n --dotenv <file> the env file to update (default .env)\n deploy Deploy (import) the manifest to a PocketBomb instance\n --url <url> --token <staff access token> [--config <file>] [--app-url <url>]\n install Install the deployed application with the scopes of the manifest (idempotent)\n --url <url> --token <staff access token> [--config <file>]\n --dotenv <file> where the credentials go without an install_url (default .env)\n info Print the manifest summary and the environment\n`;\n\ntype Io = { log: (line: string) => void; error: (line: string) => void };\n\nconst defaultIo: Io = {\n log: (line) => process.stdout.write(line + \"\\n\"),\n error: (line) => process.stderr.write(line + \"\\n\"),\n};\n\nfunction readPackageVersion(): string {\n try {\n return JSON.parse(fs.readFileSync(new URL(\"../../package.json\", import.meta.url), \"utf8\"))\n .version;\n } catch {\n return \"unknown\";\n }\n}\n\nasync function waitForApp(url: string, child: ChildProcess, timeout = 60000): Promise<void> {\n const deadline = Date.now() + timeout;\n while (Date.now() < deadline) {\n if (child.exitCode !== null) {\n throw new Error(\"The app exited early (code \" + child.exitCode + \").\");\n }\n try {\n const res = await fetch(url, { redirect: \"manual\" });\n if (res.status < 500) {\n return;\n }\n } catch {\n // not listening yet\n }\n // poll interval of the app startup readiness check\n await new Promise((r) => setTimeout(r, 150));\n }\n throw new Error(\"The app didn't start in \" + timeout + \"ms.\");\n}\n\nfunction staffClient(url: string, token: string): DashboardClient {\n const client = new DashboardClient(url, { authStore: new AuthStore() });\n client.authStore.save(token.replace(/^Bearer /, \"\"), null);\n return client;\n}\n\nfunction devVersion(manifest: AppManifest): string {\n const digest = createHash(\"sha256\")\n .update(JSON.stringify(manifest))\n .digest(\"hex\")\n .substring(0, 12);\n return (manifest.version || \"0.0.0\").split(\"-\")[0] + \"-dev.\" + digest;\n}\n\nfunction isLoopback(url: string): boolean {\n return [\"localhost\", \"127.0.0.1\", \"[::1]\"].includes(new URL(url).hostname);\n}\n\nexport interface DevOptions {\n cwd?: string;\n config?: string;\n seed?: string;\n entry?: string;\n port?: number;\n envFile?: string;\n /** An existing server to use instead of fuse (default to $POCKETBOMB_URL). */\n url?: string;\n /** The staff access token of the existing server (default to $POCKETBOMB_TOKEN). */\n token?: string;\n /** A persistent data dir of fuse (the seed runs only once per data dir). */\n dataDir?: string;\n /** The port of fuse (default to a free one). */\n fusePort?: number;\n io?: Io;\n /** Called once the app is installed (the CLI waits for the app process otherwise). */\n onReady?: (state: DevState) => void | Promise<void>;\n}\n\nexport interface DevState {\n pocketbombUrl: string;\n appUrl: string;\n issuer: string;\n installationId: string;\n applicationId: string;\n notified: boolean;\n app: ChildProcess;\n /** The fuse Owner (null with an existing server). */\n owner: { email: string; password: string } | null;\n stop(): Promise<void>;\n}\n\n/**\n * `detonator dev`: fuse -> import the manifest (with the dev app URL) as a custom application ->\n * run the app -> install it with the manifest scopes (the credentials are delivered to the running\n * app install_url, or written to the env file for a manifest without install_url).\n */\nexport async function dev(options: DevOptions = {}): Promise<DevState> {\n const io = options.io || defaultIo;\n const cwd = path.resolve(options.cwd || process.cwd());\n const manifestFile = path.resolve(cwd, options.config || \"pocketbomb.app.toml\");\n const envFile = path.resolve(cwd, options.envFile || \".env\");\n const entry = path.resolve(cwd, options.entry || \"server.ts\");\n\n const { freePort, signInStaff, startFuse } = await import(\"pocketbomb-fuse/node\");\n\n const serverUrl = (options.url || process.env.POCKETBOMB_URL || \"\").replace(/\\/+$/, \"\");\n const token = options.token || process.env.POCKETBOMB_TOKEN;\n if (serverUrl && !token) {\n throw new Error(\n \"detonator dev: --token (or $POCKETBOMB_TOKEN) is required with an existing server.\",\n );\n }\n\n const port = options.port || (await freePort(\"localhost\"));\n const appUrl = \"http://localhost:\" + port;\n const manifest = resolveManifest(loadManifest(manifestFile), appUrl);\n\n const fuse = serverUrl\n ? null\n : await startFuse({\n seed: options.seed ? path.resolve(cwd, options.seed) : undefined,\n dataDir: options.dataDir ? path.resolve(cwd, options.dataDir) : undefined,\n port: options.fusePort,\n log: (line) => io.error(\"[fuse] \" + line),\n });\n const pocketbombUrl = fuse ? fuse.url : serverUrl;\n io.error(\n fuse\n ? \"[detonator] PocketBomb (fuse) running on \" + fuse.url\n : \"[detonator] using the PocketBomb server \" + serverUrl,\n );\n\n let app: ChildProcess | null = null;\n const stop = async () => {\n if (app && app.exitCode === null) {\n const exited = new Promise((r) => app!.once(\"exit\", r));\n app.kill(\"SIGTERM\");\n await exited;\n }\n await fuse?.stop();\n };\n\n try {\n const owner = fuse\n ? await signInStaff(fuse.url, fuse.owner.email, fuse.owner.password)\n : staffClient(serverUrl, token!);\n\n // the dev app (and its webhooks) are on a loopback host; an existing server keeps its\n // settings unless it is a local one, which couldn't reach the dev app otherwise\n if (fuse) {\n await owner.settings.update({\n meta: { appURL: fuse.url },\n webhooks: { allowPrivateTargets: true },\n });\n } else if (isLoopback(serverUrl)) {\n await owner.settings.update({ webhooks: { allowPrivateTargets: true } });\n }\n\n // the versions are immutable and a dev manifest changes with the port: on a server\n // that outlives this run the dev version is derived from the manifest content\n if (!fuse) {\n manifest.version = devVersion(manifest);\n }\n\n const imported = await owner.applications.importManifest(manifest as any, \"json\");\n io.error(\"[detonator] imported \" + manifest.handle + \" \" + imported.version.version);\n\n const collections = await owner.collections.getFullList({ fields: \"id,name\" });\n const permissions = scopesToPermissions(manifest.access?.scopes || [], collections);\n const issuer = pocketbombUrl + \"/oauth2/_applicationInstallations\";\n\n const baseEnv: Record<string, string> = {\n ...fuse?.env,\n POCKETBOMB_URL: pocketbombUrl,\n POCKETBOMB_ISSUER: issuer,\n POCKETBOMB_APP_HANDLE: manifest.handle,\n APP_URL: appUrl,\n PORT: String(port),\n };\n\n const startApp = async (env: Record<string, string>) => {\n app = spawn(process.execPath, [entry], {\n cwd,\n stdio: \"inherit\",\n env: { ...process.env, ...env, NODE_ENV: process.env.NODE_ENV || \"development\" },\n });\n await waitForApp(appUrl + \"/\", app);\n io.error(\"[detonator] app running on \" + appUrl);\n };\n\n // an existing server may already have it installed (a previous `detonator dev`): new\n // credentials, because the secrets are only available when they are issued\n const existing = fuse\n ? null\n : (await owner.applications.getList()).items.find((a) => a.id === imported.application.id)\n ?.installation;\n const installApp = async () => {\n if (!existing) {\n return owner.applications.install(imported.application.id, permissions as any);\n }\n await owner.applications.update(existing.id, {\n permissions: permissions as any,\n versionRef: imported.version.id,\n });\n return owner.applications.rotateSecret(existing.id);\n };\n\n let installed;\n let envValues: Record<string, string>;\n if (manifest.install_url) {\n // the credentials are delivered to the running app (install notification)\n await startApp(baseEnv);\n installed = await installApp();\n envValues = { ...baseEnv, POCKETBOMB_INSTALLATION_ID: installed.installation.id };\n } else {\n // a custom app without install_url gets the credentials once: env file + app env\n installed = await installApp();\n envValues = {\n ...baseEnv,\n POCKETBOMB_INSTALLATION_ID: installed.installation.id,\n POCKETBOMB_CLIENT_ID: installed.clientId,\n POCKETBOMB_CLIENT_SECRET: installed.clientSecret,\n POCKETBOMB_WEBHOOK_SECRET: installed.webhookSecret,\n };\n await startApp(envValues);\n }\n\n updateEnvFile(envFile, envValues);\n io.error(\n \"[detonator] installed \" +\n manifest.handle +\n \" (installation \" +\n installed.installation.id +\n (installed.notified\n ? \", credentials delivered to the install_url\"\n : \", credentials in \" + path.basename(envFile)) +\n \")\",\n );\n\n const state: DevState = {\n pocketbombUrl,\n appUrl,\n issuer,\n installationId: installed.installation.id,\n applicationId: imported.application.id,\n notified: installed.notified,\n app: app!,\n owner: fuse?.owner ?? null,\n stop,\n };\n\n await options.onReady?.(state);\n\n return state;\n } catch (err) {\n await stop();\n throw err;\n }\n}\n\nasync function runDev(argv: Array<string>, io: Io): Promise<number> {\n const { values } = parseArgs({\n args: argv,\n options: {\n config: { type: \"string\" },\n url: { type: \"string\" },\n token: { type: \"string\" },\n seed: { type: \"string\" },\n \"data-dir\": { type: \"string\" },\n \"fuse-port\": { type: \"string\" },\n entry: { type: \"string\" },\n port: { type: \"string\" },\n dotenv: { type: \"string\" },\n },\n });\n\n const state = await dev({\n config: values.config,\n url: values.url,\n token: values.token,\n seed: values.seed,\n dataDir: values[\"data-dir\"],\n fusePort: values[\"fuse-port\"] ? parseInt(values[\"fuse-port\"], 10) : undefined,\n entry: values.entry,\n port: values.port ? parseInt(values.port, 10) : undefined,\n envFile: values.dotenv,\n io,\n });\n io.log(\n \"Dashboard: \" +\n state.pocketbombUrl +\n \"/_/\" +\n (state.owner ? \" (\" + state.owner.email + \" / \" + state.owner.password + \")\" : \"\"),\n );\n io.log(\"App: \" + state.appUrl);\n\n const code: number = await new Promise((resolve) => {\n state.app.once(\"exit\", (c) => resolve(c ?? 0));\n const shutdown = () => void state.stop().then(() => resolve(0));\n process.once(\"SIGINT\", shutdown);\n process.once(\"SIGTERM\", shutdown);\n });\n await state.stop();\n return code;\n}\n\nasync function runDeploy(argv: Array<string>, io: Io): Promise<number> {\n const { values } = parseArgs({\n args: argv,\n options: {\n url: { type: \"string\" },\n token: { type: \"string\" },\n config: { type: \"string\" },\n \"app-url\": { type: \"string\" },\n },\n });\n\n const url = values.url || process.env.POCKETBOMB_URL;\n const token = values.token || process.env.POCKETBOMB_TOKEN;\n if (!url || !token) {\n io.error(\n \"detonator deploy: --url and --token are required (or $POCKETBOMB_URL / $POCKETBOMB_TOKEN)\",\n );\n return 2;\n }\n\n const manifest = resolveManifest(\n loadManifest(path.resolve(values.config || \"pocketbomb.app.toml\")),\n values[\"app-url\"],\n );\n\n const result = await deployManifest({ url, token, manifest });\n io.log(\n (result.created ? \"created \" : result.unchanged ? \"unchanged \" : \"new version of \") +\n manifest.handle +\n \" \" +\n (result.version?.version || \"\") +\n \" (application \" +\n result.application?.id +\n \")\",\n );\n return 0;\n}\n\nasync function runInstall(argv: Array<string>, io: Io): Promise<number> {\n const { values } = parseArgs({\n args: argv,\n options: {\n url: { type: \"string\" },\n token: { type: \"string\" },\n config: { type: \"string\" },\n dotenv: { type: \"string\" },\n },\n });\n\n const url = values.url || process.env.POCKETBOMB_URL;\n const token = values.token || process.env.POCKETBOMB_TOKEN;\n if (!url || !token) {\n io.error(\n \"detonator install: --url and --token are required (or $POCKETBOMB_URL / $POCKETBOMB_TOKEN)\",\n );\n return 2;\n }\n\n const manifest = loadManifest(path.resolve(values.config || \"pocketbomb.app.toml\"));\n const owner = staffClient(url.replace(/\\/+$/, \"\"), token);\n\n const application = (await owner.applications.getList()).items.find(\n (a) => a.handle === manifest.handle,\n );\n if (!application) {\n io.error(\"detonator install: \" + manifest.handle + \" isn't deployed (run detonator deploy).\");\n return 1;\n }\n if (application.installation) {\n io.log(\n manifest.handle + \" is already installed (installation \" + application.installation.id + \")\",\n );\n return 0;\n }\n\n const collections = await owner.collections.getFullList({ fields: \"id,name\" });\n const permissions = scopesToPermissions(manifest.access?.scopes || [], collections);\n const installed = await owner.applications.install(application.id, permissions as any);\n\n if (installed.notified) {\n io.log(\n \"installed \" +\n manifest.handle +\n \" (installation \" +\n installed.installation.id +\n \", credentials delivered to the install_url)\",\n );\n return 0;\n }\n\n // returned only this once: never printed\n const envFile = path.resolve(values.dotenv || \".env\");\n updateEnvFile(envFile, {\n POCKETBOMB_URL: url.replace(/\\/+$/, \"\"),\n POCKETBOMB_INSTALLATION_ID: installed.installation.id,\n POCKETBOMB_CLIENT_ID: installed.clientId,\n POCKETBOMB_CLIENT_SECRET: installed.clientSecret,\n POCKETBOMB_WEBHOOK_SECRET: installed.webhookSecret,\n });\n io.log(\n \"installed \" +\n manifest.handle +\n \" (installation \" +\n installed.installation.id +\n \", credentials in \" +\n path.basename(envFile) +\n \")\",\n );\n return 0;\n}\n\nfunction runInfo(argv: Array<string>, io: Io): number {\n const { values } = parseArgs({ args: argv, options: { config: { type: \"string\" } } });\n const file = path.resolve(values.config || \"pocketbomb.app.toml\");\n\n io.log(\"pocketbomb-detonator \" + readPackageVersion());\n io.log(\"node \" + process.version);\n\n let manifest: AppManifest;\n try {\n manifest = loadManifest(file);\n } catch (err) {\n io.log(\"manifest \" + String((err as Error).message));\n return 1;\n }\n\n io.log(\"manifest \" + path.basename(file));\n io.log(\"handle \" + manifest.handle + \" \" + (manifest.version || \"\"));\n io.log(\"application_url \" + (manifest.application_url || \"(not set)\"));\n io.log(\"scopes \" + (manifest.access?.scopes || []).join(\", \"));\n io.log(\n \"webhooks \" +\n (manifest.webhooks?.subscriptions || [])\n .map((s) => s.topics.join(\",\") + \" -> \" + s.uri)\n .join(\"; \"),\n );\n io.log(\n \"extensions \" +\n (manifest.extensions || []).map((e) => e.type + \":\" + e.handle + \" \" + e.path).join(\"; \"),\n );\n io.log(\"POCKETBOMB_URL \" + (process.env.POCKETBOMB_URL || \"(not set)\"));\n io.log(\n \"POCKETBOMB_CLIENT_SECRET \" + (process.env.POCKETBOMB_CLIENT_SECRET ? \"(set)\" : \"(not set)\"),\n );\n return 0;\n}\n\n/**\n * Runs the CLI with the provided arguments (without the node and script paths).\n */\nexport async function run(argv: Array<string>, io: Io = defaultIo): Promise<number> {\n const [command, ...rest] = argv;\n\n switch (command) {\n case \"dev\":\n return runDev(rest, io);\n case \"deploy\":\n return runDeploy(rest, io);\n case \"install\":\n return runInstall(rest, io);\n case \"info\":\n return runInfo(rest, io);\n case \"--version\":\n case \"-v\":\n io.log(readPackageVersion());\n return 0;\n case undefined:\n case \"help\":\n case \"--help\":\n case \"-h\":\n io.log(HELP);\n return 0;\n default:\n io.error(\"Unknown command: \" + command + \"\\n\\n\" + HELP);\n return 2;\n }\n}\n"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
- import { AppManifest, deployManifest, formatEnv, loadManifest, resolveManifest, scopesToPermissions, updateEnvFile } from "./manifest.cjs";
1
+ import { scopesToPermissions } from "../scopes.cjs";
2
+ import { AppManifest, deployManifest, formatEnv, loadManifest, resolveManifest, updateEnvFile } from "./manifest.cjs";
2
3
  import { ChildProcess } from "node:child_process";
3
4
  type Io = {
4
5
  log: (line: string) => void;
@@ -11,6 +12,14 @@ export interface DevOptions {
11
12
  entry?: string;
12
13
  port?: number;
13
14
  envFile?: string;
15
+ /** An existing server to use instead of fuse (default to $POCKETBOMB_URL). */
16
+ url?: string;
17
+ /** The staff access token of the existing server (default to $POCKETBOMB_TOKEN). */
18
+ token?: string;
19
+ /** A persistent data dir of fuse (the seed runs only once per data dir). */
20
+ dataDir?: string;
21
+ /** The port of fuse (default to a free one). */
22
+ fusePort?: number;
14
23
  io?: Io;
15
24
  /** Called once the app is installed (the CLI waits for the app process otherwise). */
16
25
  onReady?: (state: DevState) => void | Promise<void>;
@@ -23,10 +32,11 @@ export interface DevState {
23
32
  applicationId: string;
24
33
  notified: boolean;
25
34
  app: ChildProcess;
35
+ /** The fuse Owner (null with an existing server). */
26
36
  owner: {
27
37
  email: string;
28
38
  password: string;
29
- };
39
+ } | null;
30
40
  stop(): Promise<void>;
31
41
  }
32
42
  /**
@@ -1,4 +1,5 @@
1
- import { AppManifest, deployManifest, formatEnv, loadManifest, resolveManifest, scopesToPermissions, updateEnvFile } from "./manifest.js";
1
+ import { scopesToPermissions } from "../scopes.js";
2
+ import { AppManifest, deployManifest, formatEnv, loadManifest, resolveManifest, updateEnvFile } from "./manifest.js";
2
3
  import { ChildProcess } from "node:child_process";
3
4
  type Io = {
4
5
  log: (line: string) => void;
@@ -11,6 +12,14 @@ export interface DevOptions {
11
12
  entry?: string;
12
13
  port?: number;
13
14
  envFile?: string;
15
+ /** An existing server to use instead of fuse (default to $POCKETBOMB_URL). */
16
+ url?: string;
17
+ /** The staff access token of the existing server (default to $POCKETBOMB_TOKEN). */
18
+ token?: string;
19
+ /** A persistent data dir of fuse (the seed runs only once per data dir). */
20
+ dataDir?: string;
21
+ /** The port of fuse (default to a free one). */
22
+ fusePort?: number;
14
23
  io?: Io;
15
24
  /** Called once the app is installed (the CLI waits for the app process otherwise). */
16
25
  onReady?: (state: DevState) => void | Promise<void>;
@@ -23,10 +32,11 @@ export interface DevState {
23
32
  applicationId: string;
24
33
  notified: boolean;
25
34
  app: ChildProcess;
35
+ /** The fuse Owner (null with an existing server). */
26
36
  owner: {
27
37
  email: string;
28
38
  password: string;
29
- };
39
+ } | null;
30
40
  stop(): Promise<void>;
31
41
  }
32
42
  /**