@promigence/sdk 0.1.0-beta.4 → 0.1.0-beta.6

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/client.js CHANGED
@@ -1,7 +1,7 @@
1
- import { lstatSync, readFileSync, readdirSync, readlinkSync, statSync } from "node:fs";
1
+ import { readFileSync, statSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { inspect } from "node:util";
4
- import { dirname, join, relative, sep } from "node:path";
4
+ import { dirname, join } from "node:path";
5
5
  import { ApiError, AuthError, EnvFailedError, PromigenceError, InvalidArgumentError, NotFoundError, NotReleasedError, NotVerifiedError, RateLimitError, SpendCapError, TimeoutError, UnreachableError, DEFAULT_API_URL, ERRORS_DOCS_URL, FILE_PUT_CHUNK_BYTES, MAX_OUTPUT_BYTES_DEFAULT, VERSION, WAIT_TIMEOUT_MS, PreviewsUnavailableError, exitForCode, previewsOff, withPreviewsFix, } from "./core.js";
6
6
  import { BUNDLE_SCHEMA, buildBundle } from "./bundle.js";
7
7
  import { keychainReadCached, keychainStore, storeLabel } from "./credstore.js";
@@ -9,6 +9,7 @@ import { CALLER_HEADER, resolveCaller, userAgentWithCaller } from "./caller.js";
9
9
  import { connectPty } from "./pty.js";
10
10
  import { connectSshTunnel } from "./ssh.js";
11
11
  import { H2Response, Http2Transport, http2Allowed } from "./http2.js";
12
+ import { CONTEXT_TAR_PREFIX } from "./context.js";
12
13
  import { proxyFetch, proxyFor } from "./proxy.js";
13
14
  import { mark, traceBegin, traceEvent } from "./trace.js";
14
15
  import { KEYED_CREATES } from "./keyed.gen.js";
@@ -407,81 +408,6 @@ export function checkImageVersioned(image) {
407
408
  throw new InvalidArgumentError(`image ${JSON.stringify(ref)} names no tag or digest, so it would be pinned to whatever ${ref}:latest is today`, `Name the version: a tag (${ref}:<version>), ${ref}:latest if you mean the moving tag, or a digest (${ref}@sha256:<64 hex>).`);
408
409
  }
409
410
  export const CONTEXT_GIT_RE = /^(?:https?:\/\/|ssh:\/\/|git:\/\/|git@)\S+@[0-9a-f]{40}$/i;
410
- export const CONTEXT_TAR_PREFIX = "tar;base64,";
411
- export const CONTEXT_SKIP = new Set([".git", "node_modules", "__pycache__", ".venv", ".DS_Store"]);
412
- export function dockerignoreMatcher(text) {
413
- const rules = [];
414
- for (const raw of text.split(/\r?\n/)) {
415
- let line = raw.trim();
416
- if (!line || line.startsWith("#"))
417
- continue;
418
- const exclusion = line.startsWith("!");
419
- if (exclusion)
420
- line = line.slice(1).trim();
421
- const parts = [];
422
- for (const seg of line.split("/")) {
423
- if (seg === "" || seg === ".")
424
- continue;
425
- if (seg === "..")
426
- parts.pop();
427
- else
428
- parts.push(seg);
429
- }
430
- const pattern = parts.join("/");
431
- if (!pattern)
432
- continue;
433
- let re = "^";
434
- for (let i = 0; i < pattern.length; i++) {
435
- const ch = pattern[i];
436
- if (ch === "*") {
437
- if (pattern[i + 1] === "*") {
438
- i++;
439
- if (pattern[i + 1] === "/")
440
- i++;
441
- re += i + 1 >= pattern.length ? ".*" : "(.*/)?";
442
- }
443
- else {
444
- re += "[^/]*";
445
- }
446
- }
447
- else if (ch === "?") {
448
- re += "[^/]";
449
- }
450
- else if (ch === "\\") {
451
- re += i + 1 < pattern.length ? `\\${pattern[++i]}` : "\\\\";
452
- }
453
- else if (ch === "[" || ch === "]") {
454
- re += ch;
455
- }
456
- else {
457
- re += /[.+()|{}$]/.test(ch) ? `\\${ch}` : ch;
458
- }
459
- }
460
- try {
461
- rules.push({ re: new RegExp(`${re}$`), exclusion });
462
- }
463
- catch {
464
- }
465
- }
466
- if (rules.length === 0)
467
- return undefined;
468
- const ignored = (rel) => {
469
- const dirs = rel.split("/");
470
- let matched = false;
471
- for (const r of rules) {
472
- if (r.exclusion !== matched)
473
- continue;
474
- let hit = r.re.test(rel);
475
- for (let i = 1; !hit && i < dirs.length; i++)
476
- hit = r.re.test(dirs.slice(0, i).join("/"));
477
- if (hit)
478
- matched = !r.exclusion;
479
- }
480
- return matched;
481
- };
482
- return { ignored, exceptions: rules.some((r) => r.exclusion) };
483
- }
484
- export const CONTEXT_MAX_BYTES = 8 * 1024 * 1024;
485
411
  export function repoUrlCredential(url) {
486
412
  const u = url.trim();
487
413
  const sep = u.indexOf("://");
@@ -531,112 +457,6 @@ export function checkDockerfileContext(context) {
531
457
  throw new InvalidArgumentError(`dockerfile.context must be a git URL pinned to a full 40-character sha, got ${JSON.stringify(context)}`, "A branch or a tag is not a pin: pass URL@<40-char sha> (git rev-parse HEAD), or pack a local " +
532
458
  "directory with packContext('./app').");
533
459
  }
534
- export function packContext(dir, opts = {}) {
535
- const max = opts.maxBytes ?? CONTEXT_MAX_BYTES;
536
- const keep = new Set([".dockerignore", ...(opts.keep ?? [])]);
537
- const collect = (ignore) => {
538
- const entries = [];
539
- let total = 0;
540
- const walk = (abs, rel) => {
541
- for (const name of readdirSync(abs).sort()) {
542
- if (CONTEXT_SKIP.has(name))
543
- continue;
544
- const childAbs = join(abs, name);
545
- const childRel = rel ? `${rel}/${name}` : name;
546
- const st = lstatSync(childAbs);
547
- const dropped = ignore !== undefined && !keep.has(childRel) && ignore.ignored(childRel);
548
- if (st.isDirectory()) {
549
- if (dropped && !ignore.exceptions)
550
- continue;
551
- if (!walk(childAbs, childRel))
552
- return false;
553
- continue;
554
- }
555
- if (dropped)
556
- continue;
557
- if (st.isSymbolicLink()) {
558
- entries.push({ path: childRel, mode: 0o777, body: Buffer.alloc(0), link: readlinkSync(childAbs) });
559
- continue;
560
- }
561
- if (!st.isFile())
562
- continue;
563
- total += st.size;
564
- if (total > max)
565
- return false;
566
- entries.push({ path: childRel, mode: st.mode & 0o111 ? 0o755 : 0o644, body: readFileSync(childAbs) });
567
- }
568
- return true;
569
- };
570
- return walk(dir, "") ? entries : undefined;
571
- };
572
- let entries = collect();
573
- if (entries === undefined) {
574
- let ignore;
575
- try {
576
- ignore = dockerignoreMatcher(readFileSync(join(dir, ".dockerignore"), "utf8"));
577
- }
578
- catch {
579
- ignore = undefined;
580
- }
581
- if (ignore !== undefined)
582
- entries = collect(ignore);
583
- if (entries === undefined) {
584
- throw new InvalidArgumentError(`the build context is over ${max} bytes${ignore ? " even without what its .dockerignore excludes" : ""}`, `${ignore ? "Exclude more in .dockerignore" : "Add a .dockerignore"}, point --context at a smaller directory, or pass a git URL pinned to a full 40-character sha.`);
585
- }
586
- }
587
- if (entries.length === 0) {
588
- throw new InvalidArgumentError(`the build context ${JSON.stringify(dir)} is empty`, "Point --context at the directory holding your Dockerfile.");
589
- }
590
- return CONTEXT_TAR_PREFIX + tarOf(entries).toString("base64");
591
- }
592
- export function contextRelative(contextDir, dockerfilePath) {
593
- const rel = relative(contextDir, dockerfilePath).split(sep).join("/");
594
- if (!rel || rel.startsWith("..")) {
595
- throw new InvalidArgumentError(`${JSON.stringify(dockerfilePath)} is not inside the context ${JSON.stringify(contextDir)}`, "Pass --context DIR with the Dockerfile underneath it.");
596
- }
597
- return rel;
598
- }
599
- function tarOf(entries) {
600
- const out = [];
601
- for (const e of [...entries].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))) {
602
- const head = Buffer.alloc(512);
603
- let name = e.path;
604
- let prefix = "";
605
- if (Buffer.byteLength(name) > 100) {
606
- const cut = name.lastIndexOf("/", name.length - 100);
607
- if (cut <= 0 || Buffer.byteLength(name.slice(cut + 1)) > 100 || Buffer.byteLength(name.slice(0, cut)) > 155) {
608
- throw new InvalidArgumentError(`the path ${JSON.stringify(e.path)} is too long for a build context`, "Shorten the path or pass a git URL pinned to a full 40-character sha.");
609
- }
610
- prefix = name.slice(0, cut);
611
- name = name.slice(cut + 1);
612
- }
613
- const put = (s, at, len) => { head.write(s.slice(0, len), at, len, "utf8"); };
614
- const oct = (n, at, len) => { put(n.toString(8).padStart(len - 1, "0") + "\0", at, len); };
615
- put(name, 0, 100);
616
- oct(e.mode, 100, 8);
617
- oct(0, 108, 8);
618
- oct(0, 116, 8);
619
- oct(e.link === undefined ? e.body.length : 0, 124, 12);
620
- oct(0, 136, 12);
621
- head.write(" ", 148, 8, "utf8");
622
- head.write(e.link === undefined ? "0" : "2", 156, 1, "utf8");
623
- if (e.link !== undefined)
624
- put(e.link, 157, 100);
625
- head.write("ustar\0", 257, 6, "binary");
626
- head.write("00", 263, 2, "utf8");
627
- put(prefix, 345, 155);
628
- let sum = 0;
629
- for (const b of head)
630
- sum += b;
631
- put(sum.toString(8).padStart(6, "0") + "\0 ", 148, 8);
632
- out.push(head);
633
- if (e.link === undefined && e.body.length > 0) {
634
- out.push(e.body, Buffer.alloc((512 - (e.body.length % 512)) % 512));
635
- }
636
- }
637
- out.push(Buffer.alloc(1024));
638
- return Buffer.concat(out);
639
- }
640
460
  function sourcesBody(sources) {
641
461
  if (sources.length > MAX_SOURCES) {
642
462
  throw new InvalidArgumentError(`snapshots.create takes at most ${MAX_SOURCES} extra sources, got ${sources.length}`, "An environment assembled from more repositories than that is cheaper to build once as " +
@@ -684,6 +504,9 @@ export function snapshotCreateBody(opts) {
684
504
  let body;
685
505
  const github = typeof o.repo === "string"
686
506
  && (o.repo.trim().startsWith(GITHUB_REPO_PREFIX) || o.repo.trim().startsWith(GITLAB_REPO_PREFIX));
507
+ if (o.env !== undefined && Object.keys(o.env).length && forms[0] === "repo") {
508
+ throw new InvalidArgumentError("snapshots.create env is for an image or a Dockerfile", "A repository snapshot sets its environment in its setup steps, or pass env on each command.");
509
+ }
687
510
  if (!github && (o.ref !== undefined || o.from !== undefined || o.track)) {
688
511
  throw new InvalidArgumentError("snapshots.create ref, from and track are for a connected repository (repo: 'github:owner/name' or 'gitlab:group/project')", "Pass { repo: 'github:your-org/app', ref: 'main' }, or drop them for this form.");
689
512
  }
@@ -739,6 +562,8 @@ export function snapshotCreateBody(opts) {
739
562
  body["tier"] = o.tier;
740
563
  if (o.registrySecret)
741
564
  body["registry_secret"] = o.registrySecret;
565
+ if (o.env !== undefined && Object.keys(o.env).length)
566
+ body["env"] = { ...o.env };
742
567
  }
743
568
  if (o.alias)
744
569
  body["alias"] = o.alias;
@@ -1485,6 +1310,11 @@ export class PromigenceClient {
1485
1310
  github = {
1486
1311
  status: () => this.request({ method: "GET", path: "/integrations/github" }),
1487
1312
  connect: () => this.request({ method: "POST", path: "/integrations/github/connect", body: {} }),
1313
+ ticket: () => this.request({ method: "POST", path: "/integrations/github/requests", body: {} }),
1314
+ request: (requestId) => this.request({
1315
+ method: "GET",
1316
+ path: `/integrations/github/requests/${encodeURIComponent(requestId)}`,
1317
+ }),
1488
1318
  disconnect: (installationId) => this.request({
1489
1319
  method: "DELETE",
1490
1320
  path: `/integrations/github/installations/${encodeURIComponent(String(installationId))}`,
@@ -1503,6 +1333,11 @@ export class PromigenceClient {
1503
1333
  gitlab = {
1504
1334
  status: () => this.request({ method: "GET", path: "/integrations/gitlab" }),
1505
1335
  connect: () => this.request({ method: "POST", path: "/integrations/gitlab/connect", body: {} }),
1336
+ ticket: () => this.request({ method: "POST", path: "/integrations/gitlab/requests", body: {} }),
1337
+ request: (requestId) => this.request({
1338
+ method: "GET",
1339
+ path: `/integrations/gitlab/requests/${encodeURIComponent(requestId)}`,
1340
+ }),
1506
1341
  disconnect: (connectionId) => this.request({
1507
1342
  method: "DELETE",
1508
1343
  path: `/integrations/gitlab/connections/${encodeURIComponent(connectionId)}`,
@@ -0,0 +1,119 @@
1
+ /**
2
+ * A local directory as a Dockerfile build context: what `packContext` sends, decided before
3
+ * anything leaves this machine.
4
+ *
5
+ * - **`.dockerignore` is applied here, always**, with the rules `docker build` uses: Go glob syntax
6
+ * plus `**`, `!` to bring a path back, the last matching line wins, a matched directory takes
7
+ * everything under it, and paths are read from the context root. A file the `.dockerignore`
8
+ * excludes is never uploaded. `<Dockerfile>.dockerignore` beside the Dockerfile is read instead
9
+ * of the root `.dockerignore` when it exists, as `docker build` does. The Dockerfile and the
10
+ * ignore files themselves are always sent, whatever a pattern says.
11
+ * - **`.git` is never sent** unless the `.dockerignore` has the line `!.git`: git history can hold
12
+ * credentials long deleted from the tree.
13
+ * - **Without a `.dockerignore`**, `node_modules`, `__pycache__`, `.venv` and `.DS_Store` are left
14
+ * out too, and a note says so; with one, only what it lists is left out.
15
+ * - Empty directories and permission bits travel as they are on disk; a set-user-ID or
16
+ * set-group-ID bit is cleared, with a note.
17
+ * - Files that look like credentials (`.env`, private keys, cloud credentials, ...) are refused,
18
+ * by name, unless `allowSecrets` says otherwise. The check goes by file name: it is
19
+ * best-effort and cannot see what a file holds, so look over the list (`listContext`) before
20
+ * you send a directory.
21
+ * - Symbolic links travel as links, never as the file they point at.
22
+ */
23
+ /** The prefix a packed directory travels under in `dockerfile.context`. */
24
+ export declare const CONTEXT_TAR_PREFIX = "tar;base64,";
25
+ /** The largest context accepted, counted in file bytes after `.dockerignore`. */
26
+ export declare const CONTEXT_MAX_BYTES: number;
27
+ /**
28
+ * Names left out when the context has no `.dockerignore`. With one, the `.dockerignore` alone
29
+ * decides, except for `.git` (sent only when it says `!.git`).
30
+ */
31
+ export declare const CONTEXT_SKIP: Set<string>;
32
+ /** The patterns of one ignore file, compiled. */
33
+ export interface DockerIgnore {
34
+ /** The ignore file these came from (`.dockerignore` or `<Dockerfile>.dockerignore`). */
35
+ readonly file: string;
36
+ /** The patterns as the build holds them, in file order: cleaned, an exception keeping its `!`. */
37
+ readonly patterns: readonly string[];
38
+ /** Whether any line is a `!` exception. */
39
+ readonly exceptions: boolean;
40
+ /** Whether a line says `!.git`, which is what sends `.git`. */
41
+ readonly sendsGit: boolean;
42
+ /**
43
+ * Is `path` (relative to the context root, `/`-separated) left out? The last pattern that
44
+ * matches it, or one of the directories above it, decides.
45
+ */
46
+ ignored(path: string): boolean;
47
+ }
48
+ /**
49
+ * Read an ignore file the way `docker build` reads it: a UTF-8 BOM is dropped; a line starting
50
+ * with `#` is a comment; each line is trimmed; `!` starts an exception; each pattern is cleaned
51
+ * (`a//b/../c` is `a/c`) and a leading `/` is dropped. A line the build cannot read (a lone `!`,
52
+ * an unclosed `[`, a trailing `\`) makes the build refuse the whole file, so it is refused here,
53
+ * naming the line, before anything is sent.
54
+ */
55
+ export declare function parseDockerignore(text: string, file?: string): DockerIgnore;
56
+ /**
57
+ * A `.dockerignore`'s patterns as a predicate, or `undefined` for a file with no patterns. Refuses
58
+ * a file the build could not read. Kept for code written against 0.1; `parseDockerignore` is the
59
+ * full reader.
60
+ */
61
+ export declare function dockerignoreMatcher(text: string): {
62
+ ignored: (rel: string) => boolean;
63
+ exceptions: boolean;
64
+ } | undefined;
65
+ /**
66
+ * Does this path look like a credential (`.env`, a private key, cloud or registry credentials, a
67
+ * token file, ...)? By name only: best-effort, it cannot see what a file holds. Templates such as
68
+ * `.env.example` are not.
69
+ */
70
+ export declare function secretLike(path: string): boolean;
71
+ /** What a build context is packed from and sent with. */
72
+ export interface ContextOptions {
73
+ /**
74
+ * The Dockerfile's path inside the context, `/`-separated (default `Dockerfile`). It is always
75
+ * sent, and `<it>.dockerignore`, when present, is read instead of the root `.dockerignore`.
76
+ */
77
+ dockerfile?: string;
78
+ /** More paths inside the context to send whatever `.dockerignore` says. */
79
+ keep?: readonly string[];
80
+ /** Refuse a context whose files add up to more than this many bytes (default `CONTEXT_MAX_BYTES`). */
81
+ maxBytes?: number;
82
+ /**
83
+ * Send files that look like credentials. Default `false`: they are refused, by name, and
84
+ * nothing is sent.
85
+ */
86
+ allowSecrets?: boolean;
87
+ /** Called with each note: what was left out by default, or changed on the way. */
88
+ onNote?: (note: string) => void;
89
+ }
90
+ /** What a build context would hold, before anything is sent. */
91
+ export interface ContextListing {
92
+ /** Every path that would be sent, sorted; a directory's ends in `/`. */
93
+ paths: string[];
94
+ /** How many of them are files or links. */
95
+ files: number;
96
+ /** The files' bytes. */
97
+ bytes: number;
98
+ /** What was left out by default or changed on the way, one sentence each. */
99
+ notes: string[];
100
+ /** The paths that would be sent and look like credentials. */
101
+ secrets: string[];
102
+ /** The ignore file applied, or `null` when there is none. */
103
+ ignoreFile: string | null;
104
+ }
105
+ /**
106
+ * What `packContext(dir, opts)` would send, without sending or packing anything: every path, the
107
+ * notes, and the files that look like credentials. Refuses only what `packContext` cannot read
108
+ * (an ignore file the build would refuse).
109
+ */
110
+ export declare function listContext(dir: string, opts?: ContextOptions): ContextListing;
111
+ /**
112
+ * Pack a directory into the string `dockerfile.context` takes. What is sent is decided here (see
113
+ * the module notes): files the `.dockerignore` excludes, and `.git`, are never uploaded; files
114
+ * that look like credentials are refused unless `allowSecrets`; a context over `maxBytes` after
115
+ * `.dockerignore` is refused. The same tree packs to the same snapshot whichever SDK packs it.
116
+ */
117
+ export declare function packContext(dir: string, opts?: ContextOptions): string;
118
+ /** The Dockerfile's path as the context sees it, for `dockerfile.path`. */
119
+ export declare function contextRelative(contextDir: string, dockerfilePath: string): string;