nimblybase 0.1.11 → 0.1.12

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.
Files changed (3) hide show
  1. package/README.md +17 -2
  2. package/dist/index.js +63 -8
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -226,6 +226,9 @@ credential configuration, private-key files, source maps, `.git` and
226
226
  `node_modules`. Symlinks and unsafe URL filenames are refused. An upload uses
227
227
  the byte snapshot that was scanned and hashed, even if a local file changes.
228
228
 
229
+ CLI 0.1.12+ uses a linear JWT scan so repeated token-like text cannot trigger
230
+ regex backtracking during preparation.
231
+
229
232
  Both site and function preparation refuse recognised management/provider
230
233
  credential patterns without printing their values. Public application keys
231
234
  remain allowed. Pattern scanning cannot detect every secret, encoded value or
@@ -235,8 +238,20 @@ private business datum; review what belongs in a public artifact.
235
238
  The preview proves local preparation only: it does not verify host installation,
236
239
  project authorization, provider runtime compatibility or live health. A
237
240
  cloud-only agent still needs a proven repository/build/upload path; connecting
238
- to MCP alone does not supply one. The checks here guard the CLI upload path;
239
- they do not add a server-side policy to direct HTTP or MCP uploads.
241
+ to MCP alone does not supply one.
242
+
243
+ The server independently refuses recognised credentials in function source
244
+ before recording/uploading code. Site uploads reject those patterns too;
245
+ manifest planning/commit refuses private paths, and commit/rollback rechecks
246
+ the actual stored bytes, hashes and sizes before changing the serving site.
247
+ Each publication reads each distinct blob once, sequentially (up to 100 MiB).
248
+ The server allows at most 9,000 distinct blobs to leave room below the default
249
+ Worker subrequest ceiling; up to 20,000 paths may share those blobs. A local
250
+ preview is not server admission. Safe existing sites remain served; an unsafe
251
+ historical release must be cleaned before it can be republished or restored.
252
+ These are accidental-leak guards, not a complete audit, malware scanner,
253
+ usage budget or protection against a project administrator rewriting R2
254
+ through their own privileged code. Function environment secrets remain valid.
240
255
 
241
256
  ## When something is broken
242
257
 
package/dist/index.js CHANGED
@@ -384,7 +384,7 @@ function when(iso) {
384
384
 
385
385
  // src/api.ts
386
386
  var DEFAULT_API_URL = "https://api-uat.nimblybase.com";
387
- var VERSION = "0.1.11";
387
+ var VERSION = "0.1.12";
388
388
  function dashboardUrl(api) {
389
389
  const match = /^https:\/\/api(-[a-z0-9]+)?\.(.+)$/.exec(api);
390
390
  return match ? `https://app${match[1] ?? ""}.${match[2]}` : null;
@@ -8737,22 +8737,78 @@ import { build as bundle } from "esbuild";
8737
8737
  // src/artifact-safety.ts
8738
8738
  import { constants } from "node:fs";
8739
8739
  import { lstat, open as open2 } from "node:fs/promises";
8740
+
8741
+ // ../../packages/contracts/dist/artifact-safety.js
8740
8742
  var CREDENTIALS = [
8741
8743
  { kind: "NimblyBase management credential", pattern: /nb_(?:svc|pat|oat|ort)_[A-Za-z0-9_-]{20,}|nb_plat_\d{1,12}_[0-9a-f]{64}/ },
8742
8744
  { kind: "provider secret key", pattern: /(?:sk|rk)_(?:test|live)_[A-Za-z0-9]{20,}/ },
8743
8745
  { kind: "AWS access key", pattern: /(?:AKIA|ASIA)[A-Z0-9]{16}/ },
8744
8746
  { kind: "GitHub credential", pattern: /gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}/ },
8745
8747
  { kind: "Slack credential", pattern: /xox[baprs]-[A-Za-z0-9-]{20,}/ },
8746
- { kind: "private key", pattern: /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY-----/ },
8747
- { kind: "signed JWT credential", pattern: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{20,}/ }
8748
+ { kind: "private key", pattern: /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY-----/ }
8748
8749
  ];
8750
+ function embeddedCredentialKind(content) {
8751
+ return CREDENTIALS.find(({ pattern }) => pattern.test(content))?.kind ?? (findSignedJwt(content) ? "signed JWT credential" : null);
8752
+ }
8753
+ function redactArtifactLabel(label) {
8754
+ const text = CREDENTIALS.reduce((text2, { pattern }) => text2.replace(new RegExp(pattern.source, "g"), "[redacted]"), label);
8755
+ let offset = 0;
8756
+ let result = "";
8757
+ let found;
8758
+ while (found = findSignedJwt(text, offset)) {
8759
+ result += text.slice(offset, found.start) + "[redacted]";
8760
+ offset = found.end;
8761
+ }
8762
+ return result + text.slice(offset);
8763
+ }
8764
+ function findSignedJwt(content, from = 0) {
8765
+ const segments = /[A-Za-z0-9_-]{10,}/g;
8766
+ segments.lastIndex = from;
8767
+ let previous = null;
8768
+ let header = null;
8769
+ for (let match = segments.exec(content); match; match = segments.exec(content)) {
8770
+ const start = match.index;
8771
+ const value = match[0];
8772
+ const end = start + value.length;
8773
+ const adjacent = previous && start === previous.end + 1 && content[previous.end] === ".";
8774
+ if (!adjacent)
8775
+ header = null;
8776
+ if (adjacent && header?.headerStart !== null && header?.headerStart !== void 0 && value.length >= 20) {
8777
+ return { start: header.headerStart, end };
8778
+ }
8779
+ const index = value.indexOf("eyJ");
8780
+ const current = { end, headerStart: index >= 0 && value.length - index >= 13 ? start + index : null };
8781
+ header = adjacent ? previous : null;
8782
+ previous = current;
8783
+ }
8784
+ return null;
8785
+ }
8786
+ var PRIVATE_ARTIFACT_NAMES = /* @__PURE__ */ new Set([
8787
+ ".git",
8788
+ "node_modules",
8789
+ ".ds_store",
8790
+ "thumbs.db",
8791
+ ".npmrc",
8792
+ ".netrc",
8793
+ ".envrc",
8794
+ ".aws",
8795
+ ".ssh",
8796
+ ".nimblybase",
8797
+ "credentials.json"
8798
+ ]);
8799
+ function excludedArtifactName(value) {
8800
+ const name = value.toLowerCase();
8801
+ return PRIVATE_ARTIFACT_NAMES.has(name) || name === ".env" || name.startsWith(".env.") || name.endsWith(".map") || /\.(?:pem|p12|pfx|key)$/.test(name) || /^id_(?:rsa|ed25519|ecdsa|dsa)$/.test(name);
8802
+ }
8803
+
8804
+ // src/artifact-safety.ts
8749
8805
  function assertNoEmbeddedCredentials(bytes, label) {
8750
8806
  const content = Buffer.from(bytes).toString("latin1");
8751
- const found = CREDENTIALS.find(({ pattern }) => pattern.test(content));
8807
+ const found = embeddedCredentialKind(content);
8752
8808
  if (found) {
8753
- const safeLabel = CREDENTIALS.reduce((text, { pattern }) => text.replace(new RegExp(pattern.source, "g"), "[redacted]"), label);
8809
+ const safeLabel = redactArtifactLabel(label);
8754
8810
  throw new CliError(
8755
- `${safeLabel} contains a recognised ${found.kind}; no artifact was uploaded.`,
8811
+ `${safeLabel} contains a recognised ${found}; no artifact was uploaded.`,
8756
8812
  EXIT.usage,
8757
8813
  "Remove credentials from the build. Use function environment secrets for private values. Public app keys are allowed; this pattern check does not detect every possible secret."
8758
8814
  );
@@ -9344,7 +9400,6 @@ import { readdir as readdir2, readFile as readFile6, lstat as lstat2 } from "nod
9344
9400
  import { createHash as createHash5 } from "node:crypto";
9345
9401
  import { spawn as spawn2 } from "node:child_process";
9346
9402
  import { join as join5, relative as relative2, resolve as resolve8, sep } from "node:path";
9347
- var SKIP = /* @__PURE__ */ new Set([".git", "node_modules", ".ds_store", "thumbs.db", ".npmrc", ".netrc", ".envrc", ".aws", ".ssh", ".nimblybase", "credentials.json"]);
9348
9403
  var MAX_FILES = 2e4;
9349
9404
  var MAX_FILE_BYTES = 25 * 1024 * 1024;
9350
9405
  var MAX_SITE_BYTES = 100 * 1024 * 1024;
@@ -9591,7 +9646,7 @@ async function collect(dir) {
9591
9646
  for (const entry of await readdir2(current, { withFileTypes: true })) {
9592
9647
  const name = entry.name.toLowerCase();
9593
9648
  const path = toUrlPath(relative2(dir, join5(current, entry.name)));
9594
- if (SKIP.has(name) || name === ".env" || name.startsWith(".env.") || name.endsWith(".map") || /\.(?:pem|p12|pfx|key)$/.test(name) || /^id_(?:rsa|ed25519|ecdsa|dsa)$/.test(name)) {
9649
+ if (excludedArtifactName(name)) {
9595
9650
  omitted.push(path);
9596
9651
  continue;
9597
9652
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nimblybase",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "The NimblyBase command line - projects, keys and backends from a terminal.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {