ninepm 0.65.0 → 0.68.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
@@ -106,7 +106,7 @@ Deploy a first-class Worker runtime app:
106
106
 
107
107
  The `--runtime worker` entry must be a bundled fetch-compatible JavaScript module with a default export. With `--with-db`, the app receives `env.DB` and should use SQLite-compatible prepared statements.
108
108
 
109
- Choose which files are public with `--assets <dir>`: only the files under that directory are served, from the app's root (`public/index.html` answers on `/`), and nothing else in the deploy directory is uploaded. Use `--no-assets` when the entry serves every response itself. The app remembers the choice, so later deploys need not repeat the flag; pass the other flag, or another directory, to change it — there is no way back to publishing everything. A worker app that has never chosen serves **every file in the deploy directory except the entry** publicly, server helpers and leftovers like `index.js.orig` included; every deploy of such an app prints a warning naming those files, and `9pm deploy --check` lists every file a deploy would make public.
109
+ Choose which files are public with `--assets <dir>`: only the files under that directory are served, from the app's root (`public/index.html` answers on `/`), and nothing else in the deploy directory is uploaded. Use `--no-assets` when the entry serves every response itself. The app remembers the choice, so later deploys need not repeat the flag; pass the other flag, or another directory, to change it — there is no way back to publishing everything. A worker app that has never chosen serves **every file in the deploy directory except the entry** publicly, server helpers and leftovers like `index.js.orig` included; every deploy of such an app prints a warning naming those files, and `9pm deploy --check` lists every file a deploy would make public. Its summary counts the files scanned apart from the files selected for upload (the entry and the public files); `--check` itself uploads nothing.
110
110
 
111
111
  Deploy a container app from a local Docker build:
112
112
 
@@ -221,6 +221,7 @@ Options:
221
221
  - `--name <name>` sets the app display name
222
222
  - `--slug <slug>` sets the URL-safe app slug
223
223
  - `--with-db` requests managed storage for the app
224
+ - `--with-auth` gives the app 9pm-managed end-user sign-in (email one-time codes) and implies `--with-db`. Once enabled it stays on for every later deploy of the app, with or without the flag; it can't be turned off
224
225
  - `--database-name <name>` customizes the requested database name
225
226
  - `--binding <name>` customizes the database binding name
226
227
  - `--environment <name>` sets deployment environment metadata
package/dist/index.d.ts CHANGED
@@ -38,10 +38,17 @@ export interface DeployResult {
38
38
  project: Project;
39
39
  deployment: Deployment;
40
40
  database: AppDatabase | null;
41
+ databases: BoundDatabase[];
41
42
  uploaded: number;
42
43
  url: string;
43
44
  containerImageBytes?: number;
44
45
  }
46
+ export interface BoundDatabase {
47
+ bindingName: string;
48
+ name: string | null;
49
+ }
50
+ export declare function boundDatabasesForDeployment(deployment: Deployment, provisioned: AppDatabase | null): BoundDatabase[];
51
+ export declare function deployDatabaseClause(databases: readonly BoundDatabase[]): string | null;
45
52
  export interface ApiOptions {
46
53
  apiUrl: string;
47
54
  token: string;
@@ -117,7 +124,7 @@ export interface CreatedServiceToken {
117
124
  clientSecret: string;
118
125
  }
119
126
  export type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
120
- export declare const CLI_VERSION = "0.65.0";
127
+ export declare const CLI_VERSION = "0.68.0";
121
128
  type CredentialBackend = "macos-keychain" | "windows-dpapi" | "linux-secret-service" | "file-plaintext" | "none";
122
129
  export interface StoredCredentialEntry {
123
130
  token: string;
@@ -191,6 +198,10 @@ export interface DeployCheckResult {
191
198
  path: string;
192
199
  url: string;
193
200
  }>;
201
+ upload?: {
202
+ files: number;
203
+ bytes: number;
204
+ };
194
205
  }
195
206
  export type DetectedFramework = "nextjs" | "astro" | "sveltekit" | "vite";
196
207
  export declare function detectFramework(directory: string): DetectedFramework | null;
@@ -230,6 +241,7 @@ export declare function measureWorkerModuleFiles(root: string, modulePaths: read
230
241
  compressedBytes: number;
231
242
  }>;
232
243
  export declare function checkDeployDirectory(options: DeployOptions): Promise<DeployCheckResult>;
244
+ export declare function deployCheckSummary(check: DeployCheckResult, publicAssets: string | undefined): string;
233
245
  export declare function whoami(options: ApiOptions): Promise<WhoamiResult>;
234
246
  export declare function publicWhoamiResult(result: WhoamiResult): PublicWhoamiResult;
235
247
  export type CredentialRevocation = {
package/dist/index.js CHANGED
@@ -16,7 +16,40 @@ import { MAX_STATIC_ASSET_FILE_BYTES, MAX_WORKER_ENTRY_COMPRESSED_BYTES, MAX_WOR
16
16
  import { buildFileManifest, readManifestFile } from "./manifest.js";
17
17
  import { NO_PUBLIC_ASSETS, normalizeAssetsDirectory, privateLookingPaths, publicAssetPath } from "./public-assets.js";
18
18
  import { API_KEY_HELP, API_URL_HELP, CLI_CLIENT_ID, DEFAULT_API_URL, reportCliError } from "./cli-config.js";
19
- export const CLI_VERSION = "0.65.0";
19
+ // What the deployment binds, read from the deployment the server returned (#1949). The
20
+ // server binds every provisioned database of the app on each publish whatever flags were
21
+ // passed, so `--with-db` says nothing about the result. Only a deployment carrying no
22
+ // `databases` record at all - the local mock target, or a server that predates recording
23
+ // it for this runtime - falls back to what this run itself provisioned.
24
+ export function boundDatabasesForDeployment(deployment, provisioned) {
25
+ const recorded = deployment.metadata["databases"];
26
+ if (!Array.isArray(recorded)) {
27
+ return provisioned ? [{ bindingName: provisioned.bindingName, name: provisioned.name }] : [];
28
+ }
29
+ const bound = [];
30
+ for (const entry of recorded) {
31
+ if (!entry || typeof entry !== "object") {
32
+ continue;
33
+ }
34
+ const record = entry;
35
+ if (typeof record["bindingName"] !== "string") {
36
+ continue;
37
+ }
38
+ const name = typeof record["name"] === "string" ? record["name"] : provisioned && record["id"] === provisioned.id ? provisioned.name : null;
39
+ bound.push({ bindingName: record["bindingName"], name });
40
+ }
41
+ return bound;
42
+ }
43
+ // The database clause of the deploy summary, or null when nothing is bound - the summary
44
+ // then says nothing about a database rather than "no database".
45
+ export function deployDatabaseClause(databases) {
46
+ if (databases.length === 0) {
47
+ return null;
48
+ }
49
+ const listed = databases.map((database) => (database.name ? `${database.bindingName}:${database.name}` : database.bindingName)).join(", ");
50
+ return `${databases.length === 1 ? "database" : "databases"} ${listed}`;
51
+ }
52
+ export const CLI_VERSION = "0.68.0";
20
53
  const PUBLIC_DASHBOARD_URL = "https://9pm.ai/dashboard";
21
54
  const TOKEN_SERVICE = "9pm";
22
55
  const TOKEN_ACCOUNT = "default";
@@ -442,6 +475,7 @@ async function runDeploy(initialOptions, slugHolder) {
442
475
  project,
443
476
  deployment: finalized,
444
477
  database,
478
+ databases: boundDatabasesForDeployment(finalized, database),
445
479
  uploaded: 1,
446
480
  url: finalized.url,
447
481
  containerImageBytes: image.size,
@@ -637,6 +671,7 @@ async function runDeploy(initialOptions, slugHolder) {
637
671
  project,
638
672
  deployment: finalized,
639
673
  database,
674
+ databases: boundDatabasesForDeployment(finalized, database),
640
675
  uploaded,
641
676
  url: finalized.url,
642
677
  };
@@ -812,9 +847,12 @@ export async function checkDeployDirectory(options) {
812
847
  // #1847: `--check` has no project, so it cannot see a choice made on an earlier deploy;
813
848
  // it reports what this invocation's flags would publish, and says so for the legacy case.
814
849
  let publicFiles;
850
+ let upload;
815
851
  if (serverEntry) {
816
852
  const selection = options.publicAssets ?? null;
817
853
  publicFiles = workerPublicFiles(manifest.map((file) => file.path), serverEntry, selection);
854
+ const selected = uploadedFiles(manifest, serverEntry, selection);
855
+ upload = { files: selected.length, bytes: selected.reduce((sum, file) => sum + file.size, 0) };
818
856
  if (selection !== null && selection !== NO_PUBLIC_ASSETS && publicFiles.length === 0) {
819
857
  errors.push(missingAssetsDirectoryMessage(selection));
820
858
  }
@@ -847,8 +885,26 @@ export async function checkDeployDirectory(options) {
847
885
  bytes: totalBytes,
848
886
  ...(serverCode ? { serverCode } : {}),
849
887
  ...(publicFiles ? { publicFiles } : {}),
888
+ ...(upload ? { upload } : {}),
850
889
  };
851
890
  }
891
+ // The `--check` success line. Static and bundle deploys upload every file they scan, so one
892
+ // count is the whole story there. An entry-based worker deploy uploads only the entry and
893
+ // its public files, so it reports both counts; with no --assets/--no-assets, a choice the
894
+ // app saved on an earlier deploy applies on the server, which a local check cannot see.
895
+ export function deployCheckSummary(check, publicAssets) {
896
+ if (!check.upload) {
897
+ return `Deploy check passed: ${check.files} file(s), ${check.bytes} byte(s).`;
898
+ }
899
+ const publicCount = check.publicFiles?.length ?? 0;
900
+ const breakdown = publicAssets === NO_PUBLIC_ASSETS ? "the entry only" : `the entry + ${publicCount} public file(s)`;
901
+ const summary = `Deploy check passed: ${check.files} file(s) scanned, ${check.upload.files} selected for upload (${breakdown}), ` +
902
+ `${check.upload.bytes.toLocaleString("en-US")} byte(s) selected.`;
903
+ if (publicAssets !== undefined) {
904
+ return summary;
905
+ }
906
+ return `${summary}\n Counted from the flags supplied (no --assets or --no-assets). If this app saved a public-assets choice on an earlier deploy, that choice applies on deploy and only the entry and the files it makes public are uploaded.`;
907
+ }
852
908
  export async function whoami(options) {
853
909
  return apiRequest(options, "/v1/me", { method: "GET" });
854
910
  }
@@ -2305,7 +2361,14 @@ function buildContainerImage(directory) {
2305
2361
  const tempDir = mkdtempSync(path.join(os.tmpdir(), "9pm-container-"));
2306
2362
  const tag = `9pm-local-${Date.now()}-${process.pid}`;
2307
2363
  const imagePath = path.join(tempDir, "image.tar");
2308
- runDocker(["build", "--platform", "linux/amd64", "-t", tag, root], "build container image");
2364
+ // Build without Buildx's default provenance attestation. With it, Docker Desktop saves the
2365
+ // image as an index carrying an attestation manifest, and the push to Cloudflare's registry
2366
+ // through `wrangler containers push` fails with "blob unknown to registry"; the same image
2367
+ // without the attestation pushes (#1906: two of two attempts each way). The environment
2368
+ // variable rather than `--provenance=false`: it is what that test pushed, and a builder that
2369
+ // has no attestations - the legacy non-Buildx `docker build` - ignores an unknown variable
2370
+ // where it would reject an unknown flag. SBOM generation is untouched: it is off by default.
2371
+ runDocker(["build", "--platform", "linux/amd64", "-t", tag, root], "build container image", { BUILDX_NO_DEFAULT_ATTESTATIONS: "1" });
2309
2372
  const inspected = spawnSync("docker", ["image", "inspect", tag, "--format", "{{.Os}}/{{.Architecture}}"], { encoding: "utf8" });
2310
2373
  if (inspected.status !== 0) {
2311
2374
  throw new Error(`Failed to inspect container image: ${inspected.stderr || inspected.stdout}`.trim());
@@ -2318,8 +2381,8 @@ function buildContainerImage(directory) {
2318
2381
  const sha256 = createHash("sha256").update(readFileSync(imagePath)).digest("hex");
2319
2382
  return { tag, path: imagePath, size, sha256 };
2320
2383
  }
2321
- function runDocker(args, action) {
2322
- const result = spawnSync("docker", args, { stdio: "inherit" });
2384
+ function runDocker(args, action, extraEnv = {}) {
2385
+ const result = spawnSync("docker", args, { stdio: "inherit", env: { ...process.env, ...extraEnv } });
2323
2386
  if (result.status !== 0) {
2324
2387
  throw new Error(`Failed to ${action}.`);
2325
2388
  }
@@ -4389,7 +4452,7 @@ async function main() {
4389
4452
  console.log("Deploy check passed: Dockerfile found and Docker is available.");
4390
4453
  }
4391
4454
  else {
4392
- console.log(`Deploy check passed: ${check.files} file(s), ${check.bytes} byte(s).`);
4455
+ console.log(deployCheckSummary(check, command.options.publicAssets));
4393
4456
  if (check.serverCode) {
4394
4457
  // The compressed server-code figure, printed on success as well as on
4395
4458
  // warning/rejection, so the number the size limit is enforced on is always
@@ -4438,10 +4501,10 @@ async function main() {
4438
4501
  const siteFlag = slug ? ` --site ${quoteSiteArg(slug)}` : "";
4439
4502
  throw new Error(`${base}\n\nStuck? Report it with context: 9pm support${siteFlag} -m "<what happened>"`);
4440
4503
  }
4441
- const databaseLine = result.database ? `database ${result.database.bindingName}:${result.database.name}` : "no database";
4504
+ const databaseClause = deployDatabaseClause(result.databases);
4442
4505
  const queued = result.deployment.status === "queued" || result.deployment.status === "publishing";
4443
4506
  console.log(`${queued ? "Queued" : "Deployed"} ${result.project.slug} -> ${result.url}`);
4444
- console.log(`Deployment ${result.deployment.id} ${result.containerImageBytes ? `uploaded ${formatBytesForCli(result.containerImageBytes)} image` : `uploaded ${result.uploaded} file(s)`}, ${databaseLine}.`);
4507
+ console.log(`Deployment ${result.deployment.id} ${result.containerImageBytes ? `uploaded ${formatBytesForCli(result.containerImageBytes)} image` : `uploaded ${result.uploaded} file(s)`}${databaseClause ? `, ${databaseClause}` : ""}.`);
4445
4508
  if (queued) {
4446
4509
  console.log(`Status: ${result.deployment.status}. Run 9pm apps to check when it becomes active.`);
4447
4510
  }
@@ -5028,7 +5091,7 @@ Container deploys:
5028
5091
  Requires Docker locally. The CLI builds linux/amd64, exports the image, uploads it through a short-lived session, then finalizes a queued deployment.
5029
5092
  The app must listen on 0.0.0.0:$PORT - always read the PORT env var; on some placements the platform holds the selected --port and hands the app a different one. Use --health-path for a cheap readiness endpoint.
5030
5093
  With --with-db, containers receive NINEPM_SQL_URL and NINEPM_DB_BINDINGS for the managed SQL bridge, plus NINEPM_SQL_TOKEN where that bridge requires one - send it as "Authorization: Bearer <token>" or every query is refused.
5031
- With --with-auth, the app gets 9pm-managed end-user accounts (email one-time codes); it implies --with-db and is one-way (enable-only).
5094
+ With --with-auth, the app gets 9pm-managed end-user accounts (email one-time codes); it implies --with-db and is one-way: once enabled it stays on for every later deploy of the app, with or without the flag.
5032
5095
  With --with-email, a signed-in end user can send app invitation emails; it implies --with-auth and works on public apps only.
5033
5096
  9pm env set <app> KEY=value [KEY2=value2 ...] [--var] (secret by default; --var = readable config)
5034
5097
  9pm env set <app> --from-file <path> [--var] (KEY=value lines; # comments, export prefix, quotes ok)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ninepm",
3
- "version": "0.65.0",
3
+ "version": "0.68.0",
4
4
  "description": "CLI for deploying built apps to 9pm.ai.",
5
5
  "homepage": "https://9pm.ai",
6
6
  "keywords": [