@fjall/util 2.20.1 → 2.22.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/dist/.minified CHANGED
@@ -1 +1 @@
1
- 66 files minified at 2026-06-23T21:58:47.448Z
1
+ 72 files minified at 2026-06-30T11:38:01.900Z
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Pure CloudFormation deploy-progress counting, shared by the CLI Ink progress
3
+ * box and the webapp deployment panel so both quantify a deploy identically.
4
+ *
5
+ * Operates on raw CloudFormation resource-status strings — no AWS SDK, no React,
6
+ * no Node — so it is safe to import from a browser bundle.
7
+ */
8
+ export interface ResourceProgressCounts {
9
+ /** Resources at a terminal `_COMPLETE` status that is not a rollback. */
10
+ completed: number;
11
+ /** Resources currently `_IN_PROGRESS`. */
12
+ inProgress: number;
13
+ /** Resources at a `_FAILED` or `ROLLBACK` status. */
14
+ failed: number;
15
+ /** Total resources seen. */
16
+ total: number;
17
+ /**
18
+ * Completed / total as a 0–100 integer. `0` when nothing has been seen.
19
+ * Callers that track a terminal "stack complete" / "empty stack" state may
20
+ * override this to 100 — this is the raw count-derived figure only.
21
+ */
22
+ percentage: number;
23
+ }
24
+ /**
25
+ * Count completed / in-progress / failed resources from their CloudFormation
26
+ * status strings and derive a percentage. Matches the CLI's long-standing
27
+ * `useStackOperationState` derivation: a resource counts as complete when its
28
+ * status includes `COMPLETE` and not `ROLLBACK`.
29
+ */
30
+ export declare function countResourceProgress(statuses: Iterable<string>): ResourceProgressCounts;
31
+ /**
32
+ * Format a wall-clock elapsed duration as `Hh Mm Ss` / `Mm Ss` / `Ss`, dropping
33
+ * leading zero units. Used for the live "elapsed" clock on both surfaces.
34
+ */
35
+ export declare function formatElapsed(ms: number): string;
@@ -0,0 +1 @@
1
+ function c(e){return e.includes("COMPLETE")&&!e.includes("ROLLBACK")}function l(e){return e.includes("FAILED")||e.includes("ROLLBACK")}function u(e){return e.includes("IN_PROGRESS")}function f(e){let n=0,r=0,o=0,t=0;for(const s of e)t++,c(s)?n++:l(s)?o++:u(s)&&r++;const i=t===0?0:Math.min(100,Math.round(n/t*100));return{completed:n,inProgress:r,failed:o,total:t,percentage:i}}function a(e){const n=Math.max(0,Math.floor(e/1e3)),r=Math.floor(n/3600),o=Math.floor(n%3600/60),t=n%60;return r>0?`${r}h ${o}m ${t}s`:o>0?`${o}m ${t}s`:`${t}s`}export{f as countResourceProgress,a as formatElapsed};
@@ -1,6 +1,8 @@
1
1
  export { AWSError, NoRolesFoundError, InvalidCredentialsError, SSOTokenExpiredError, MissingRegionError, ProfileNotFoundError, CommandError, isAWSError, isNoRolesFoundError, isSSOUnauthorizedError } from "./errors.js";
2
2
  export { STACK_NOT_FOUND_PATTERN, CDK_NO_STACKS_MATCH, type ResourceEvent, isResourceEvent } from "./cloudformationTypes.js";
3
+ export { type ResourceProgressCounts, countResourceProgress, formatElapsed } from "./deployProgress.js";
3
4
  export { CloudFormationFailureAnalyser, type RootCause, type FailureAnalysis } from "./CloudFormationFailureAnalyser.js";
5
+ export { maskFailureAnalysis } from "./maskFailureAnalysis.js";
4
6
  export { IPAM_OPERATIONS_POOL_TAG_KEY, formatIpamPairTagValue } from "./ipamTags.js";
5
7
  export { SDK_PRE_EMPTY_TAG_KEY } from "./infraTags.js";
6
8
  export { ACCOUNT_MONITORING_ROLE_NAME } from "./monitoringRole.js";
package/dist/aws/index.js CHANGED
@@ -1 +1 @@
1
- import{AWSError as e,NoRolesFoundError as E,InvalidCredentialsError as _,SSOTokenExpiredError as i,MissingRegionError as T,ProfileNotFoundError as n,CommandError as A,isAWSError as O,isNoRolesFoundError as a,isSSOUnauthorizedError as t}from"./errors.js";import{STACK_NOT_FOUND_PATTERN as m,CDK_NO_STACKS_MATCH as s,isResourceEvent as S}from"./cloudformationTypes.js";import{CloudFormationFailureAnalyser as l}from"./CloudFormationFailureAnalyser.js";import{IPAM_OPERATIONS_POOL_TAG_KEY as R,formatIpamPairTagValue as f}from"./ipamTags.js";import{SDK_PRE_EMPTY_TAG_KEY as u}from"./infraTags.js";import{ACCOUNT_MONITORING_ROLE_NAME as x}from"./monitoringRole.js";export{x as ACCOUNT_MONITORING_ROLE_NAME,e as AWSError,s as CDK_NO_STACKS_MATCH,l as CloudFormationFailureAnalyser,A as CommandError,R as IPAM_OPERATIONS_POOL_TAG_KEY,_ as InvalidCredentialsError,T as MissingRegionError,E as NoRolesFoundError,n as ProfileNotFoundError,u as SDK_PRE_EMPTY_TAG_KEY,i as SSOTokenExpiredError,m as STACK_NOT_FOUND_PATTERN,f as formatIpamPairTagValue,O as isAWSError,a as isNoRolesFoundError,S as isResourceEvent,t as isSSOUnauthorizedError};
1
+ import{AWSError as e,NoRolesFoundError as E,InvalidCredentialsError as s,SSOTokenExpiredError as a,MissingRegionError as i,ProfileNotFoundError as _,CommandError as t,isAWSError as m,isNoRolesFoundError as n,isSSOUnauthorizedError as A}from"./errors.js";import{STACK_NOT_FOUND_PATTERN as O,CDK_NO_STACKS_MATCH as l,isResourceEvent as N}from"./cloudformationTypes.js";import{countResourceProgress as p,formatElapsed as u}from"./deployProgress.js";import{CloudFormationFailureAnalyser as d}from"./CloudFormationFailureAnalyser.js";import{maskFailureAnalysis as x}from"./maskFailureAnalysis.js";import{IPAM_OPERATIONS_POOL_TAG_KEY as P,formatIpamPairTagValue as F}from"./ipamTags.js";import{SDK_PRE_EMPTY_TAG_KEY as K}from"./infraTags.js";import{ACCOUNT_MONITORING_ROLE_NAME as g}from"./monitoringRole.js";export{g as ACCOUNT_MONITORING_ROLE_NAME,e as AWSError,l as CDK_NO_STACKS_MATCH,d as CloudFormationFailureAnalyser,t as CommandError,P as IPAM_OPERATIONS_POOL_TAG_KEY,s as InvalidCredentialsError,i as MissingRegionError,E as NoRolesFoundError,_ as ProfileNotFoundError,K as SDK_PRE_EMPTY_TAG_KEY,a as SSOTokenExpiredError,O as STACK_NOT_FOUND_PATTERN,p as countResourceProgress,u as formatElapsed,F as formatIpamPairTagValue,m as isAWSError,n as isNoRolesFoundError,N as isResourceEvent,A as isSSOUnauthorizedError,x as maskFailureAnalysis};
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Credential masking for CloudFormation FailureAnalysis at the producer
3
+ * boundary. The analysis embeds raw CloudFormation `statusReason` strings
4
+ * (`rootCause.reason`, `rootCause.resource.statusReason`, each
5
+ * `affectedResources[].statusReason`) which can carry credential fragments —
6
+ * a failed `GetSecretValue`, a connection string in an error body, etc.
7
+ *
8
+ * The engine masks the analysis once, here, before it reaches any sink
9
+ * (the `onFailureAnalysis` callback, `DeployResult.failureAnalysis`, the file
10
+ * event log, and the webapp's persisted column), so no consumer has to re-mask.
11
+ *
12
+ * `summary`, `dependencyChain`, `remediation`, `errorPattern` and the resource
13
+ * identifiers (`logicalId`, `resourceType`, `physicalId`) are names/canned text,
14
+ * not credential values, so they pass through unchanged (cf. the
15
+ * "values, not identifiers" masking rule).
16
+ */
17
+ import type { FailureAnalysis } from "./CloudFormationFailureAnalyser.js";
18
+ export declare function maskFailureAnalysis(analysis: FailureAnalysis): FailureAnalysis;
@@ -0,0 +1 @@
1
+ import{maskSensitiveOutput as o}from"../securityHelpers.js";function r(e){return e.statusReason===void 0?e:{...e,statusReason:o(e.statusReason)}}function u(e){const s={...e.rootCause,reason:o(e.rootCause.reason),resource:r(e.rootCause.resource)};return{...e,rootCause:s,affectedResources:e.affectedResources.map(r)}}export{u as maskFailureAnalysis};
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The exact stdout prefix `fjall deploy --plan` prints the engine approval
3
+ * token under, and the MCP `plan_deployment` handler parses to capture it for
4
+ * cross-process replay.
5
+ *
6
+ * Single-sourced here because the CLI print site (`@fjall/cli`) and the MCP
7
+ * parse site (`@fjall/mcp`) live in different packages that share no test — a
8
+ * reworded literal at one site would otherwise break MCP plan-token capture
9
+ * silently. `@fjall/util` is the lowest common dependency of both.
10
+ */
11
+ export declare const APPROVAL_TOKEN_OUTPUT_PREFIX = "Approval token: ";
@@ -0,0 +1 @@
1
+ const o="Approval token: ";export{o as APPROVAL_TOKEN_OUTPUT_PREFIX};
@@ -19,6 +19,7 @@
19
19
  */
20
20
  import { type ChildProcess } from "node:child_process";
21
21
  import type { BuildxBuildArgs, BuildxBuildResult, DockerCliError, DockerCliErrorKind } from "./dockerCliSchemas.js";
22
+ import type { EcrAuthSession } from "./ecrCredentialStore.js";
22
23
  import { failure, success, type Result } from "./result.js";
23
24
  export interface DockerCliLogger {
24
25
  debug(category: string, message: string, data?: Record<string, unknown>): void;
@@ -37,6 +38,15 @@ export interface DockerCliState {
37
38
  readonly dockerBin: string;
38
39
  readonly env: NodeJS.ProcessEnv;
39
40
  readonly abortSignal: AbortSignal | undefined;
41
+ /**
42
+ * Ephemeral ECR credential context. `loginEcr` establishes it (an isolated
43
+ * `DOCKER_CONFIG` carrying the ECR token inline, never the OS keychain);
44
+ * `logoutEcr` tears it down. `spawnEnv` merges `session.env` over `env` on
45
+ * every docker subprocess so the credentials reach buildx push / docker push
46
+ * without touching the keychain. Mutable by design — the login→build→push
47
+ * flow is sequential on a given DockerCli instance.
48
+ */
49
+ ecrSession: EcrAuthSession | undefined;
40
50
  }
41
51
  export interface BuildxProgressEvent {
42
52
  readonly type: "vertex" | "log" | "status" | "warning";
@@ -97,6 +107,13 @@ export declare class DockerCli {
97
107
  }, DockerCliError>>;
98
108
  imagetoolsInspect(image: string): Promise<Result<ImagetoolsInspect, DockerCliError>>;
99
109
  loginEcr(args: EcrLoginArgs): Promise<Result<void, DockerCliError>>;
110
+ /**
111
+ * Tear down the ephemeral ECR credential context established by `loginEcr`
112
+ * (removes the temp `DOCKER_CONFIG` dir). Callers MUST invoke this in a
113
+ * `finally` after a build/push so the deploy-scoped token file does not leak.
114
+ * Best-effort: never throws.
115
+ */
116
+ logoutEcr(): Promise<void>;
100
117
  ensureBuilder(name: string): Promise<Result<{
101
118
  created: boolean;
102
119
  }, DockerCliError>>;
@@ -1 +1 @@
1
- import{spawn as b}from"node:child_process";import{filterDangerousEnvVars as B,maskSensitiveOutput as E}from"../securityHelpers.js";import{abortChildProcess as h}from"./abortHelpers.js";import{DEFAULT_DOCKER_BIN as k,DOCKER_CLI_LOG_CATEGORY as A,STDERR_TAIL_LINE_MAX_CHARS as y,STDERR_TAIL_LINES as _}from"./dockerCliConstants.js";import{_buildxBuild as x,_imagetoolsInspect as D,_tagByDigest as w}from"./DockerCli.build.js";import{_assertBuildxAvailable as C,_detectDaemon as I,_ensureBuilder as L}from"./DockerCli.daemon.js";import{_imageInspect as R,_loginEcr as O,_pull as P,_push as N,_tag as F}from"./DockerCli.registry.js";import{failure as G,success as K}from"./result.js";class M{state;constructor(e){const r=e.dockerBin!==void 0&&e.dockerBin!==""?e.dockerBin:k,n=B(e.env??process.env);this.state={logger:e.logger,dockerBin:r,env:n,abortSignal:e.abortSignal}}async buildxBuild(e,r){return x(this.state,e,r)}async tag(e,r){return F(this.state,e,r)}async tagByDigest(e,r,n){return w(this.state,e,r,n)}async push(e,r){return N(this.state,e,r)}async pull(e,r,n){return P(this.state,e,r,n)}async imageInspect(e){return R(this.state,e)}async imagetoolsInspect(e){return D(this.state,e)}async loginEcr(e){return O(this.state,e)}async ensureBuilder(e){return L(this.state,e)}async assertBuildxAvailable(){return C(this.state)}async detectDaemon(){return I(this.state)}}function j(t){const e=E(t);return e.length>y?e.slice(0,y)+"\u2026":e}function H(t,e,r){let n,l=r;if(r!==void 0&&Array.isArray(r.stderrTail)&&r.stderrTail.every(o=>typeof o=="string")){n=r.stderrTail.map(j);const{stderrTail:o,...s}=r;l=Object.keys(s).length>0?s:void 0}return{kind:t,message:e,...n!==void 0&&{stderrTail:n},...l!==void 0&&{details:l}}}function Q(t){return H("daemon_unreachable",`Docker CLI is not available: ${t.spawnError??"unknown spawn failure"}`,{stderrTail:t.stderrTail})}function v(t,e){if(t==="")return[];const r=t.split(/\r?\n/);for(;r.length>0&&r[r.length-1]==="";)r.pop();return r.slice(-e)}function W(t){return E(t)}function Z(t,e){return new Promise(r=>{let n=!1,l=!1;const o=T(t.abortSignal,e.timeoutMs);let s;try{s=b(t.dockerBin,e.args,{shell:!1,env:t.env})}catch(i){const a=i instanceof Error?i.message:String(i);r({exitCode:null,stdout:"",stderr:a,stderrTail:[a],aborted:!1,spawnError:a});return}let c="",u="";s.stdout?.on("data",i=>{c+=i.toString()}),s.stderr?.on("data",i=>{u+=i.toString()}),e.stdin!==void 0&&(s.stdin?.on("error",i=>{t.logger.debug(A,"stdin write error (typically EPIPE on early child exit)",{error:i.message})}),s.stdin?.write(e.stdin),s.stdin?.end());const g=()=>{n=!0,h(s)};o!==void 0&&(o.aborted?g():o.addEventListener("abort",g,{once:!0})),s.once("error",i=>{if(l)return;l=!0,o!==void 0&&o.removeEventListener("abort",g);const a=i instanceof Error?i.message:String(i);r({exitCode:null,stdout:c,stderr:a,stderrTail:[a],aborted:n,spawnError:a})}),s.once("close",i=>{l||(l=!0,o!==void 0&&o.removeEventListener("abort",g),r({exitCode:i,stdout:c,stderr:u,stderrTail:v(u,_),aborted:n}))})})}function ee(t,e,r,n){return new Promise(l=>{let o=!1,s=!1;const c=T(t.abortSignal,e.timeoutMs);let u;try{u=b(t.dockerBin,e.args,{shell:!1,env:t.env})}catch(d){const f=d instanceof Error?d.message:String(d);l({exitCode:null,stderrTail:[f],aborted:!1,spawnError:f,child:null});return}let g="",i="",a="";u.stdout?.on("data",d=>{g+=d.toString();const f=g.split(/\r?\n/);g=f.pop()??"";for(const p of f)r(p)}),u.stderr?.on("data",d=>{const f=d.toString();if(i+=f,n!==void 0){a+=f;const p=a.split(/\r?\n/);a=p.pop()??"";for(const S of p)n(S)}});const m=()=>{o=!0,h(u)};c!==void 0&&(c.aborted?m():c.addEventListener("abort",m,{once:!0})),u.once("error",d=>{if(s)return;s=!0,c!==void 0&&c.removeEventListener("abort",m);const f=d instanceof Error?d.message:String(d);l({exitCode:null,stderrTail:[f],aborted:o,spawnError:f,child:u})}),u.once("close",d=>{s||(s=!0,g!==""&&r(g),n!==void 0&&a!==""&&n(a),c!==void 0&&c.removeEventListener("abort",m),l({exitCode:d,stderrTail:v(i,_),aborted:o,child:u}))})})}function T(t,e){if(!(t===void 0&&e===void 0))return t===void 0?AbortSignal.timeout(e):e===void 0?t:AbortSignal.any([t,AbortSignal.timeout(e)])}export{M as DockerCli,G as failure,H as makeError,W as maskOutput,Z as runDocker,Q as spawnFailureToError,ee as streamDocker,K as success,v as tailLines};
1
+ import{spawn as E}from"node:child_process";import{filterDangerousEnvVars as k,maskSensitiveOutput as h}from"../securityHelpers.js";import{abortChildProcess as b}from"./abortHelpers.js";import{DEFAULT_DOCKER_BIN as A,DOCKER_CLI_LOG_CATEGORY as w,STDERR_TAIL_LINE_MAX_CHARS as y,STDERR_TAIL_LINES as v}from"./dockerCliConstants.js";import{_buildxBuild as x,_imagetoolsInspect as D,_tagByDigest as C}from"./DockerCli.build.js";import{_assertBuildxAvailable as I,_detectDaemon as L,_ensureBuilder as R}from"./DockerCli.daemon.js";import{_imageInspect as O,_loginEcr as P,_pull as N,_push as F,_tag as G}from"./DockerCli.registry.js";import{failure as K,success as j}from"./result.js";class Q{state;constructor(e){const t=e.dockerBin!==void 0&&e.dockerBin!==""?e.dockerBin:A,n=k(e.env??process.env);this.state={logger:e.logger,dockerBin:t,env:n,abortSignal:e.abortSignal,ecrSession:void 0}}async buildxBuild(e,t){return x(this.state,e,t)}async tag(e,t){return G(this.state,e,t)}async tagByDigest(e,t,n){return C(this.state,e,t,n)}async push(e,t){return F(this.state,e,t)}async pull(e,t,n){return N(this.state,e,t,n)}async imageInspect(e){return O(this.state,e)}async imagetoolsInspect(e){return D(this.state,e)}async loginEcr(e){return P(this.state,e)}async logoutEcr(){const e=this.state.ecrSession;this.state.ecrSession=void 0,e!==void 0&&await e.dispose()}async ensureBuilder(e){return R(this.state,e)}async assertBuildxAvailable(){return I(this.state)}async detectDaemon(){return L(this.state)}}function H(r){const e=h(r);return e.length>y?e.slice(0,y)+"\u2026":e}function U(r,e,t){let n,u=t;if(t!==void 0&&Array.isArray(t.stderrTail)&&t.stderrTail.every(o=>typeof o=="string")){n=t.stderrTail.map(H);const{stderrTail:o,...i}=t;u=Object.keys(i).length>0?i:void 0}return{kind:r,message:e,...n!==void 0&&{stderrTail:n},...u!==void 0&&{details:u}}}function W(r){return U("daemon_unreachable",`Docker CLI is not available: ${r.spawnError??"unknown spawn failure"}`,{stderrTail:r.stderrTail})}function S(r,e){if(r==="")return[];const t=r.split(/\r?\n/);for(;t.length>0&&t[t.length-1]==="";)t.pop();return t.slice(-e)}function Z(r){return h(r)}function _(r){return r.ecrSession!==void 0?{...r.env,...r.ecrSession.env}:r.env}function ee(r,e){return new Promise(t=>{let n=!1,u=!1;const o=T(r.abortSignal,e.timeoutMs);let i;try{i=E(r.dockerBin,e.args,{shell:!1,env:_(r)})}catch(s){const a=s instanceof Error?s.message:String(s);t({exitCode:null,stdout:"",stderr:a,stderrTail:[a],aborted:!1,spawnError:a});return}let c="",l="";i.stdout?.on("data",s=>{c+=s.toString()}),i.stderr?.on("data",s=>{l+=s.toString()}),e.stdin!==void 0&&(i.stdin?.on("error",s=>{r.logger.debug(w,"stdin write error (typically EPIPE on early child exit)",{error:s.message})}),i.stdin?.write(e.stdin),i.stdin?.end());const g=()=>{n=!0,b(i)};o!==void 0&&(o.aborted?g():o.addEventListener("abort",g,{once:!0})),i.once("error",s=>{if(u)return;u=!0,o!==void 0&&o.removeEventListener("abort",g);const a=s instanceof Error?s.message:String(s);t({exitCode:null,stdout:c,stderr:a,stderrTail:[a],aborted:n,spawnError:a})}),i.once("close",s=>{u||(u=!0,o!==void 0&&o.removeEventListener("abort",g),t({exitCode:s,stdout:c,stderr:l,stderrTail:S(l,v),aborted:n}))})})}function re(r,e,t,n){return new Promise(u=>{let o=!1,i=!1;const c=T(r.abortSignal,e.timeoutMs);let l;try{l=E(r.dockerBin,e.args,{shell:!1,env:_(r)})}catch(d){const f=d instanceof Error?d.message:String(d);u({exitCode:null,stderrTail:[f],aborted:!1,spawnError:f,child:null});return}let g="",s="",a="";l.stdout?.on("data",d=>{g+=d.toString();const f=g.split(/\r?\n/);g=f.pop()??"";for(const p of f)t(p)}),l.stderr?.on("data",d=>{const f=d.toString();if(s+=f,n!==void 0){a+=f;const p=a.split(/\r?\n/);a=p.pop()??"";for(const B of p)n(B)}});const m=()=>{o=!0,b(l)};c!==void 0&&(c.aborted?m():c.addEventListener("abort",m,{once:!0})),l.once("error",d=>{if(i)return;i=!0,c!==void 0&&c.removeEventListener("abort",m);const f=d instanceof Error?d.message:String(d);u({exitCode:null,stderrTail:[f],aborted:o,spawnError:f,child:l})}),l.once("close",d=>{i||(i=!0,g!==""&&t(g),n!==void 0&&a!==""&&n(a),c!==void 0&&c.removeEventListener("abort",m),u({exitCode:d,stderrTail:S(s,v),aborted:o,child:l}))})})}function T(r,e){if(!(r===void 0&&e===void 0))return r===void 0?AbortSignal.timeout(e):e===void 0?r:AbortSignal.any([r,AbortSignal.timeout(e)])}export{Q as DockerCli,K as failure,U as makeError,Z as maskOutput,ee as runDocker,W as spawnFailureToError,re as streamDocker,j as success,S as tailLines};
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * Registry-related implementations for `DockerCli`.
3
3
  *
4
- * `loginEcr` shells `docker login --username <u> --password-stdin <registry>`
5
- * with the password sent on stdin so it never appears in argv (visible via
6
- * `ps`). `imageInspect` distinguishes "image not found" (kind:inspect_failed
4
+ * `loginEcr` no longer shells `docker login` (its keychain credential-store
5
+ * step is the deploy-blocking `-25299` failure on macOS). It establishes an
6
+ * ephemeral, isolated `DOCKER_CONFIG` carrying the ECR token inline — see
7
+ * `_loginEcr` and `ecrCredentialStore.ts`. `imageInspect` distinguishes
8
+ * "image not found" (kind:inspect_failed
7
9
  * with `exists: false`) from other inspect failures by sniffing the
8
10
  * stderr tail for the literal `No such image:` substring docker emits.
9
11
  */
@@ -16,4 +18,14 @@ export declare function _imageInspect(state: DockerCliState, image: string): Pro
16
18
  exists: boolean;
17
19
  digest?: string;
18
20
  }, DockerCliError>>;
21
+ /**
22
+ * Establish an ephemeral, isolated ECR credential context instead of shelling
23
+ * `docker login`. `docker login` would store the token via the configured
24
+ * `credsStore` (the macOS osxkeychain helper), which fails to overwrite a
25
+ * stale entry (`errSecDuplicateItem -25299`) and blocks every deploy. This
26
+ * writes a deploy-scoped `DOCKER_CONFIG` carrying the token inline (no
27
+ * credsStore, keychain never touched) and records it on the state so every
28
+ * subsequent docker/buildx subprocess inherits it. `DockerCli.logoutEcr`
29
+ * tears it down. See `ecrCredentialStore.ts` for the full rationale.
30
+ */
19
31
  export declare function _loginEcr(state: DockerCliState, args: EcrLoginArgs): Promise<Result<void, DockerCliError>>;
@@ -1,3 +1,3 @@
1
- import{maskSensitiveOutput as h}from"../securityHelpers.js";import{DEFAULT_DAEMON_PROBE_TIMEOUT_MS as E,DEFAULT_INSPECT_TIMEOUT_MS as x,DEFAULT_PULL_TIMEOUT_MS as w,DEFAULT_PUSH_TIMEOUT_MS as C,DOCKER_CLI_LOG_CATEGORY as $}from"./dockerCliConstants.js";import{failure as i,makeError as a,maskOutput as _,runDocker as T,spawnFailureToError as c,streamDocker as g,success as u}from"./DockerCli.js";async function M(o,r,e){const t=await T(o,{args:["tag",r,e],timeoutMs:x});return t.spawnError!==void 0?i(c(t)):t.exitCode===null?i(a("abort",`docker tag ${r} ${e} was aborted`,{stderrTail:t.stderrTail})):t.exitCode!==0?i(a("tag_failed",`docker tag ${r} ${e} failed (exit ${t.exitCode})`,{stderrTail:t.stderrTail})):u(void 0)}async function D(o,r,e){let t;const d=await g(o,{args:["push",r],timeoutMs:C},f=>{if(f==="")return;let s;try{s=JSON.parse(f)}catch{o.logger.debug($,"Skipping malformed push progress line",{line:h(f)});return}if(s===null||typeof s!="object")return;const n=s;n.aux?.digest!==void 0&&(t=n.aux.digest),e!==void 0&&n.id!==void 0&&n.status!==void 0&&e({id:n.id,status:_(n.status),...n.progressDetail?.current!==void 0&&{current:n.progressDetail.current},...n.progressDetail?.total!==void 0&&{total:n.progressDetail.total}})});return d.spawnError!==void 0?i(c(d)):d.aborted||d.exitCode===null?i(a("abort",`docker push ${r} was aborted`,{stderrTail:d.stderrTail})):d.exitCode!==0?d.stderrTail.join(`
2
- `).toLowerCase().includes("unauthorized")?i(a("auth_failed",`docker push ${r} unauthorized`,{stderrTail:d.stderrTail})):i(a("push_failed",`docker push ${r} failed (exit ${d.exitCode})`,{stderrTail:d.stderrTail})):t===void 0?i(a("push_failed",`docker push ${r} succeeded but no digest was reported`,{stderrTail:d.stderrTail})):u({digest:t})}async function L(o,r,e,t){const l=["pull"];e!==void 0&&e!==""&&l.push("--platform",e),l.push(r);let d="";const s=await g(o,{args:l,timeoutMs:w},p=>{d+=`${p}
3
- `,!(t===void 0||p==="")&&t({id:r,status:_(p)})});if(s.spawnError!==void 0)return i(c(s));if(s.aborted||s.exitCode===null)return i(a("abort",`docker pull ${r} was aborted`,{stderrTail:s.stderrTail}));if(s.exitCode!==0)return i(a("pull_failed",`docker pull ${r} failed (exit ${s.exitCode})`,{stderrTail:s.stderrTail}));const n=/sha256:[0-9a-f]{64}/.exec(d);return u({imageId:n?.[0]??r})}async function O(o,r){const e=await T(o,{args:["image","inspect","--format","{{json .}}",r],timeoutMs:x});if(e.spawnError!==void 0)return i(c(e));if(e.exitCode===null)return i(a("abort",`docker image inspect ${r} was aborted`,{stderrTail:e.stderrTail}));if(e.exitCode!==0)return e.stderr.includes("No such image:")?u({exists:!1}):i(a("inspect_failed",`docker image inspect ${r} failed (exit ${e.exitCode})`,{stderrTail:e.stderrTail}));const t=/"Id":"(sha256:[0-9a-f]{64})"/.exec(e.stdout);return u({exists:!0,...t?.[1]!==void 0&&{digest:t[1]}})}async function y(o,r){const e=await T(o,{args:["login","--username",r.username,"--password-stdin",r.registry],stdin:r.password,timeoutMs:E});return e.spawnError!==void 0?i(c(e)):e.exitCode===null?i(a("abort",`docker login ${r.registry} was aborted`)):e.exitCode!==0?i(a("auth_failed",`docker login ${r.registry} failed (exit ${e.exitCode})`,{stderrTail:e.stderrTail})):u(void 0)}export{O as _imageInspect,y as _loginEcr,L as _pull,D as _push,M as _tag};
1
+ import{maskSensitiveOutput as T}from"../securityHelpers.js";import{DEFAULT_INSPECT_TIMEOUT_MS as x,DEFAULT_PULL_TIMEOUT_MS as m,DEFAULT_PUSH_TIMEOUT_MS as E,DOCKER_CLI_LOG_CATEGORY as w}from"./dockerCliConstants.js";import{createEcrAuthSession as $}from"./ecrCredentialStore.js";import{failure as i,makeError as a,maskOutput as g,runDocker as _,spawnFailureToError as f,streamDocker as h,success as u}from"./DockerCli.js";async function M(n,r,e){const t=await _(n,{args:["tag",r,e],timeoutMs:x});return t.spawnError!==void 0?i(f(t)):t.exitCode===null?i(a("abort",`docker tag ${r} ${e} was aborted`,{stderrTail:t.stderrTail})):t.exitCode!==0?i(a("tag_failed",`docker tag ${r} ${e} failed (exit ${t.exitCode})`,{stderrTail:t.stderrTail})):u(void 0)}async function S(n,r,e){let t;const s=await h(n,{args:["push",r],timeoutMs:E},c=>{if(c==="")return;let d;try{d=JSON.parse(c)}catch{n.logger.debug(w,"Skipping malformed push progress line",{line:T(c)});return}if(d===null||typeof d!="object")return;const o=d;o.aux?.digest!==void 0&&(t=o.aux.digest),e!==void 0&&o.id!==void 0&&o.status!==void 0&&e({id:o.id,status:g(o.status),...o.progressDetail?.current!==void 0&&{current:o.progressDetail.current},...o.progressDetail?.total!==void 0&&{total:o.progressDetail.total}})});return s.spawnError!==void 0?i(f(s)):s.aborted||s.exitCode===null?i(a("abort",`docker push ${r} was aborted`,{stderrTail:s.stderrTail})):s.exitCode!==0?s.stderrTail.join(`
2
+ `).toLowerCase().includes("unauthorized")?i(a("auth_failed",`docker push ${r} unauthorized`,{stderrTail:s.stderrTail})):i(a("push_failed",`docker push ${r} failed (exit ${s.exitCode})`,{stderrTail:s.stderrTail})):t===void 0?i(a("push_failed",`docker push ${r} succeeded but no digest was reported`,{stderrTail:s.stderrTail})):u({digest:t})}async function y(n,r,e,t){const l=["pull"];e!==void 0&&e!==""&&l.push("--platform",e),l.push(r);let s="";const d=await h(n,{args:l,timeoutMs:m},p=>{s+=`${p}
3
+ `,!(t===void 0||p==="")&&t({id:r,status:g(p)})});if(d.spawnError!==void 0)return i(f(d));if(d.aborted||d.exitCode===null)return i(a("abort",`docker pull ${r} was aborted`,{stderrTail:d.stderrTail}));if(d.exitCode!==0)return i(a("pull_failed",`docker pull ${r} failed (exit ${d.exitCode})`,{stderrTail:d.stderrTail}));const o=/sha256:[0-9a-f]{64}/.exec(s);return u({imageId:o?.[0]??r})}async function D(n,r){const e=await _(n,{args:["image","inspect","--format","{{json .}}",r],timeoutMs:x});if(e.spawnError!==void 0)return i(f(e));if(e.exitCode===null)return i(a("abort",`docker image inspect ${r} was aborted`,{stderrTail:e.stderrTail}));if(e.exitCode!==0)return e.stderr.includes("No such image:")?u({exists:!1}):i(a("inspect_failed",`docker image inspect ${r} failed (exit ${e.exitCode})`,{stderrTail:e.stderrTail}));const t=/"Id":"(sha256:[0-9a-f]{64})"/.exec(e.stdout);return u({exists:!0,...t?.[1]!==void 0&&{digest:t[1]}})}async function O(n,r){try{const e=await $({registry:r.registry,username:r.username,password:r.password,baseEnv:n.env,logger:n.logger}),t=n.ecrSession;return n.ecrSession=e,t!==void 0&&await t.dispose(),u(void 0)}catch(e){return i(a("auth_failed",`Failed to configure ECR credentials for ${r.registry}: ${T(e instanceof Error?e.message:String(e))}`))}}export{D as _imageInspect,O as _loginEcr,y as _pull,S as _push,M as _tag};
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Bake-guard for `docker.buildArgs` — the advisory tripwire that keeps a
3
+ * credential out of the `--build-arg` channel.
4
+ *
5
+ * A `--build-arg` value is baked into the image and PUBLISHED to three surfaces
6
+ * (named verbatim in every finding/warning so the author sees the blast radius):
7
+ * - `docker history --no-trunc` of the pushed image,
8
+ * - image provenance / SBOM attestations, and
9
+ * - the `<app>-cache` mode=max ECR repo, which exports every intermediate
10
+ * layer to a SEPARATE registry repository.
11
+ *
12
+ * The guard is an ADVISORY tripwire, not a wall. The SSM/Secrets-Manager-ref
13
+ * reject (R1) is the primary signal; the `maskSensitiveOutput`-shape reject
14
+ * (R2) is one CHEAP heuristic that catches the well-known credential shapes
15
+ * (AWS keys, `password=…`, `postgres://u:p@h`) but does NOT catch base64 blobs,
16
+ * bare high-entropy tokens, or a credential concatenated across two innocuous
17
+ * `buildArgs`. No admission guard catches those — the real controls are the
18
+ * documented Dockerfile contract (use `RUN --mount=type=secret`) plus the
19
+ * least-privilege build-secret identity (design § C1).
20
+ *
21
+ * Public, design § "Research validation & mandatory corrections" C3:
22
+ * `acknowledgePublic: true` opts a value out of R1/R2 — a conscious
23
+ * public-but-sensitive bake (restricted Stripe pk, scoped Mapbox token, Sentry
24
+ * DSN) must not fight the guard.
25
+ */
26
+ import type { DockerBuildArgValue } from "../manifest/schemas.js";
27
+ /**
28
+ * The exposure clause naming the three publication surfaces. Shared so every
29
+ * finding/warning message states the same blast radius (design R4).
30
+ */
31
+ export declare const BAKE_GUARD_EXPOSURE_CLAUSE: string;
32
+ export type BakeGuardFindingReason = "sourced-ref" | "credential-shape";
33
+ export interface BakeGuardFinding {
34
+ /** The offending `buildArgs` key. */
35
+ key: string;
36
+ reason: BakeGuardFindingReason;
37
+ /** Author-facing remediation message — already names the exposure surfaces. */
38
+ message: string;
39
+ }
40
+ export interface BakeGuardWarning {
41
+ key: string;
42
+ message: string;
43
+ }
44
+ export interface BakeGuardResult {
45
+ findings: BakeGuardFinding[];
46
+ warnings: BakeGuardWarning[];
47
+ }
48
+ /**
49
+ * Evaluate `buildArgs` against the four bake-guard rules.
50
+ *
51
+ * - **R1 (reject):** an `ssm`/`secretsManager`-sourced value belongs in
52
+ * `buildSecrets`. An `env`-sourced value is allowed — shell sourcing of
53
+ * public config is the documented per-deploy override path. Bypassed by
54
+ * `acknowledgePublic: true` (a deliberate public-but-sensitive bake).
55
+ * - **R2 (reject):** a literal-string value whose `maskSensitiveOutput`-masked
56
+ * form differs from the original (a credential SHAPE) belongs in
57
+ * `buildSecrets`. Bypassed by `acknowledgePublic: true` — but a plain string
58
+ * carries no acknowledgement, so for a string the only escape is to move it
59
+ * to the object form and acknowledge, or to `buildSecrets`.
60
+ * - **R3 (warn, non-blocking):** a non-secret key NOT starting with a public
61
+ * prefix is frozen into the image at build time; a runtime env var varies
62
+ * per deploy without a rebuild.
63
+ *
64
+ * `appPrefix` is the app-name kebab prefix used only for messaging context (it
65
+ * does not change which rules fire); pass the kebab app name.
66
+ */
67
+ export declare function evaluateBakeGuard(buildArgs: Record<string, DockerBuildArgValue> | undefined, appPrefix: string): BakeGuardResult;
68
+ /**
69
+ * The set of `buildArgs` keys whose DECLARED value is an acknowledged
70
+ * public-but-sensitive object form (`acknowledgePublic: true`). These are
71
+ * exempt from the post-resolution value-shape reject, mirroring the
72
+ * pre-resolution R2 bypass — a deliberate public-but-sensitive bake must not
73
+ * fight the guard at either stage.
74
+ */
75
+ export declare function acknowledgedBuildArgKeys(buildArgs: Record<string, DockerBuildArgValue> | undefined): Set<string>;
76
+ /**
77
+ * Post-resolution value-shape check (R2 only). Runs the
78
+ * `maskSensitiveOutput`-shape reject on a RESOLVED `buildArgs` map (keys mapped
79
+ * to their final string values) and returns credential-shape findings.
80
+ *
81
+ * This closes the composition gap the pre-resolution `evaluateBakeGuard` cannot
82
+ * see: a `.env`-inferred key (discovered during resolution) or an explicit key
83
+ * whose `.env`/shell-resolved value is secret-shaped never reaches the declared
84
+ * pre-resolution pass. Callers run this AFTER `resolveBuildArgs` and fail closed
85
+ * on any finding.
86
+ *
87
+ * Value-shape only — it does NOT emit R3 prefix-warnings (those stay at the
88
+ * pre-resolution pass to avoid double-warning). Keys in `acknowledgedKeys` are
89
+ * skipped (an acknowledged public-but-sensitive bake is intentional).
90
+ */
91
+ export declare function evaluateResolvedBuildArgValues(resolvedBuildArgs: Record<string, string>, appPrefix: string, acknowledgedKeys: ReadonlySet<string>): BakeGuardFinding[];
@@ -0,0 +1 @@
1
+ import{maskSensitiveOutput as s}from"../securityHelpers.js";import{PUBLIC_BUILD_ARG_PREFIXES as l,isPublicBuildVarName as u}from"./buildArgInference.js";const o="baked values are world-readable in `docker history`, image provenance/SBOM, and the `<app>-cache` mode=max ECR cache repo";function p(t,n){const r=[],i=[];if(t===void 0)return{findings:r,warnings:i};for(const[e,a]of Object.entries(t)){if(typeof a=="object"){if(a.acknowledgePublic===!0)continue;if(a.ssm!==void 0||a.secretsManager!==void 0){r.push({key:e,reason:"sourced-ref",message:`buildArg "${e}" (app "${n}") is sourced from a secret store (SSM/Secrets Manager) but baked via --build-arg \u2014 ${o}. Move it to docker.buildSecrets (BuildKit --secret mount, never baked), or, if the value is genuinely public-but-sensitive and must ship in the client bundle, set acknowledgePublic: true to bake it deliberately.`});continue}u(e)||i.push({key:e,message:d(e)});continue}if(s(a)!==a){r.push({key:e,reason:"credential-shape",message:c(e,n)});continue}u(e)||i.push({key:e,message:d(e)})}return{findings:r,warnings:i}}function d(t){return`buildArg "${t}" does not start with a public prefix (${l.join(", ")}) and is frozen into the image at build time \u2014 ${o}. If this value should vary per deploy, use a runtime env var (ECS task environment) instead of baking it.`}function c(t,n){return`buildArg "${t}" (app "${n}") has a value that looks like a credential but would be baked via --build-arg \u2014 ${o}. Move it to docker.buildSecrets (BuildKit --secret mount, never baked). If it is a genuinely public-but-sensitive value, use the object form { ssm/secretsManager/env, acknowledgePublic: true } to bake it deliberately.`}function g(t){const n=new Set;if(t===void 0)return n;for(const[r,i]of Object.entries(t))typeof i=="object"&&i.acknowledgePublic===!0&&n.add(r);return n}function v(t,n,r){const i=[];for(const[e,a]of Object.entries(t))r.has(e)||s(a)!==a&&i.push({key:e,reason:"credential-shape",message:c(e,n)});return i}export{o as BAKE_GUARD_EXPOSURE_CLAUSE,g as acknowledgedBuildArgKeys,p as evaluateBakeGuard,v as evaluateResolvedBuildArgValues};
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Public-config build-arg inference — the single source of truth for the
3
+ * "public prefix" predicate and the helper that infers which public build-arg
4
+ * keys a service implicitly declares.
5
+ *
6
+ * A build-arg key starting with a public prefix (`VITE_`, `NEXT_PUBLIC_`,
7
+ * `PUBLIC_`) is world-readable by design: a bundler inlines its value into the
8
+ * client bundle at build time. That is the EXPECTED use of `--build-arg`, and
9
+ * it is the only mechanism by which `import.meta.env.VITE_*` values reach the
10
+ * production client bundle (container ENV cannot reach the client).
11
+ *
12
+ * This module OWNS `PUBLIC_BUILD_ARG_PREFIXES` (the bake-guard imports it from
13
+ * here, resolving the circular dependency the inference helper would otherwise
14
+ * create). Phase 3 widens the build-arg key set in exactly one gated way: a
15
+ * `.env`-FILE key or a declared service-environment key matching a public
16
+ * prefix becomes a DECLARED build-arg. Raw shell env is never a key source.
17
+ */
18
+ import type { DockerBuildArgValue } from "../manifest/schemas.js";
19
+ /**
20
+ * Public-config prefixes. A `buildArgs` key starting with one of these is
21
+ * world-readable by design (a bundler inlines it into the client bundle), so it
22
+ * is the expected use of `--build-arg` and does not trip the bake-guard's
23
+ * non-secret warning (R3). Single source of truth — consumed by the synth-time
24
+ * guard, the pre-build guard, the bake-guard predicate, and the inference
25
+ * helper below.
26
+ */
27
+ export declare const PUBLIC_BUILD_ARG_PREFIXES: readonly ["VITE_", "NEXT_PUBLIC_", "PUBLIC_"];
28
+ /**
29
+ * True iff `key` starts with one of the public build-arg prefixes
30
+ * (`VITE_`, `NEXT_PUBLIC_`, `PUBLIC_`).
31
+ */
32
+ export declare function isPublicBuildVarName(key: string): boolean;
33
+ export interface InferPublicBuildArgKeysInput {
34
+ /**
35
+ * The declared service environment (synth-time, source 1). Keys with a public
36
+ * prefix here become declared build-args — they MUST bake even without a
37
+ * `.env` file (the synth path write-through supplies the value).
38
+ */
39
+ declaredEnv?: Record<string, string>;
40
+ /**
41
+ * Parsed `.env`/`.env.<stage>` FILE keys (resolve-time, source 2). A public
42
+ * prefix here promotes the key to a declared build-arg; the value is supplied
43
+ * by the resolver's precedence ladder, not by this helper.
44
+ */
45
+ dotenvKeys?: readonly string[];
46
+ /**
47
+ * Already-explicit `docker.buildArgs`. A key present here is never inferred —
48
+ * the explicit declaration always wins (and may carry an acknowledged
49
+ * public-but-sensitive object form the inference must not clobber).
50
+ */
51
+ explicitBuildArgs?: Record<string, DockerBuildArgValue>;
52
+ }
53
+ /**
54
+ * Infer the set of public-prefixed build-arg keys a service implicitly
55
+ * declares, from its declared environment and its parsed `.env` file keys.
56
+ *
57
+ * Returns prefix-matched keys from `Object.keys(declaredEnv) ∪ dotenvKeys`,
58
+ * MINUS any key already present in `explicitBuildArgs`, deduplicated. Pure — no
59
+ * fs, no env reads; the caller supplies both sources.
60
+ */
61
+ export declare function inferPublicBuildArgKeys(input: InferPublicBuildArgKeysInput): string[];
@@ -0,0 +1 @@
1
+ const c=["VITE_","NEXT_PUBLIC_","PUBLIC_"];function i(e){return c.some(t=>e.startsWith(t))}function s(e){const t=e.explicitBuildArgs??{},r=new Set([...Object.keys(e.declaredEnv??{}),...e.dotenvKeys??[]]),o=[];for(const n of r)i(n)&&(Object.prototype.hasOwnProperty.call(t,n)||o.push(n));return o}export{c as PUBLIC_BUILD_ARG_PREFIXES,s as inferPublicBuildArgKeys,i as isPublicBuildVarName};
@@ -1 +1 @@
1
- import{DEFAULT_BUILDER_NAME as i}from"./dockerCliConstants.js";function p(e){const t=["buildx","build"];t.push("--builder",e.builder??i),t.push("--progress=rawjson"),t.push("--metadata-file",e.metadataFile),t.push("--platform",e.platforms.join(","));for(const u of e.tags)t.push("-t",u);t.push("-f",e.dockerfilePath),e.pushByDigest===!0&&e.imageName!==void 0&&t.push("--output",`type=image,name=${e.imageName},push-by-digest=true,push=true`);for(const[u,o]of Object.entries(e.buildArgs))t.push("--build-arg",`${u}=${o}`);if(e.target!==void 0&&t.push("--target",e.target),e.cacheFrom!==void 0)for(const u of e.cacheFrom)t.push("--cache-from",u);if(e.cacheTo!==void 0)for(const u of e.cacheTo)t.push("--cache-to",u);if(e.secrets!==void 0)for(const u of e.secrets)t.push("--secret",`id=${u.id},src=${u.source}`);return t.push(`--provenance=${e.provenance?"true":"false"}`),t.push(`--sbom=${e.sbom?"true":"false"}`),e.pushByDigest!==!0&&(e.push&&t.push("--push"),e.load&&t.push("--load")),t.push(e.contextPath),t}export{p as buildxArgvBuilder};
1
+ import{DEFAULT_BUILDER_NAME as i}from"./dockerCliConstants.js";function c(e){const u=["buildx","build"];u.push("--builder",e.builder??i),u.push("--progress=rawjson"),u.push("--metadata-file",e.metadataFile),u.push("--platform",e.platforms.join(","));for(const t of e.tags)u.push("-t",t);u.push("-f",e.dockerfilePath),e.pushByDigest===!0&&e.imageName!==void 0&&u.push("--output",`type=image,name=${e.imageName},push-by-digest=true,push=true`);for(const[t,o]of Object.entries(e.buildArgs))u.push("--build-arg",`${t}=${o}`);if(e.dockerfileChecks!==void 0&&e.dockerfileChecks.length>0&&u.push("--build-arg",`BUILDKIT_DOCKERFILE_CHECK=${e.dockerfileChecks.join(";")}`),e.target!==void 0&&u.push("--target",e.target),e.cacheFrom!==void 0)for(const t of e.cacheFrom)u.push("--cache-from",t);if(e.cacheTo!==void 0)for(const t of e.cacheTo)u.push("--cache-to",t);if(e.secrets!==void 0)for(const t of e.secrets){const o=t.source.kind==="file"?`src=${t.source.path}`:`env=${t.source.name}`;u.push("--secret",`id=${t.id},${o}`)}return u.push(`--provenance=${e.provenance?"true":"false"}`),u.push(`--sbom=${e.sbom?"true":"false"}`),e.pushByDigest!==!0&&(e.push&&u.push("--push"),e.load&&u.push("--load")),u.push(e.contextPath),u}export{c as buildxArgvBuilder};
@@ -46,8 +46,15 @@ export declare const BuildxBuildArgsSchema: z.ZodObject<{
46
46
  cacheTo: z.ZodOptional<z.ZodArray<z.ZodString>>;
47
47
  secrets: z.ZodOptional<z.ZodArray<z.ZodObject<{
48
48
  id: z.ZodString;
49
- source: z.ZodString;
49
+ source: z.ZodDiscriminatedUnion<[z.ZodObject<{
50
+ kind: z.ZodLiteral<"file">;
51
+ path: z.ZodString;
52
+ }, z.core.$strict>, z.ZodObject<{
53
+ kind: z.ZodLiteral<"env">;
54
+ name: z.ZodString;
55
+ }, z.core.$strict>], "kind">;
50
56
  }, z.core.$strict>>>;
57
+ dockerfileChecks: z.ZodOptional<z.ZodArray<z.ZodString>>;
51
58
  provenance: z.ZodBoolean;
52
59
  sbom: z.ZodBoolean;
53
60
  push: z.ZodBoolean;
@@ -1 +1 @@
1
- import{z as e}from"zod";const i=e.enum(["buildx_unavailable","daemon_unreachable","build_failed","push_failed","pull_failed","tag_failed","inspect_failed","auth_failed","abort","timeout","validation","metadata_missing","metadata_malformed","metadata_missing_digest"]),a=new Set(i.options);function r(t){return a.has(t)}const s=e.object({contextPath:e.string().min(1),dockerfilePath:e.string().min(1),target:e.string().min(1).optional(),platforms:e.array(e.string().min(1)).min(1),tags:e.array(e.string().min(1)),buildArgs:e.record(e.string(),e.string()),cacheFrom:e.array(e.string().min(1)).optional(),cacheTo:e.array(e.string().min(1)).optional(),secrets:e.array(e.object({id:e.string().min(1),source:e.string().min(1)}).strict()).optional(),provenance:e.boolean(),sbom:e.boolean(),push:e.boolean(),load:e.boolean(),pushByDigest:e.boolean().optional(),imageName:e.string().min(1).optional(),builder:e.string().min(1,"Builder name cannot be empty").optional(),metadataFile:e.string().min(1)}).strict().refine(t=>!(t.platforms.length>1&&t.load),{message:"Multi-arch builds cannot use load:true (Docker limitation); use push:true or omit both for cache-only"}).refine(t=>!(t.push&&t.load),{message:"push and load are mutually exclusive"}).refine(t=>t.pushByDigest!==!0||t.imageName!==void 0,{message:"pushByDigest requires imageName (the registry repo to push to)"}).refine(t=>t.pushByDigest===!0||t.tags.length>=1,{message:"tags must be non-empty unless pushByDigest is set"}),o=e.object({digest:e.string(),imageDigests:e.record(e.string(),e.string()),metadata:e.record(e.string(),e.unknown()),platforms:e.array(e.string()),cacheHits:e.number().int().nonnegative(),cacheMisses:e.number().int().nonnegative()}).strict(),l=e.object({kind:i,message:e.string(),stderrTail:e.array(e.string()).optional(),details:e.unknown().optional()}).strict();export{s as BuildxBuildArgsSchema,o as BuildxBuildResultSchema,i as DockerCliErrorKindSchema,l as DockerCliErrorSchema,r as isDockerCliErrorKind};
1
+ import{z as e}from"zod";const i=e.enum(["buildx_unavailable","daemon_unreachable","build_failed","push_failed","pull_failed","tag_failed","inspect_failed","auth_failed","abort","timeout","validation","metadata_missing","metadata_malformed","metadata_missing_digest"]),n=new Set(i.options);function r(t){return n.has(t)}const s=e.object({contextPath:e.string().min(1),dockerfilePath:e.string().min(1),target:e.string().min(1).optional(),platforms:e.array(e.string().min(1)).min(1),tags:e.array(e.string().min(1)),buildArgs:e.record(e.string(),e.string()),cacheFrom:e.array(e.string().min(1)).optional(),cacheTo:e.array(e.string().min(1)).optional(),secrets:e.array(e.object({id:e.string().min(1),source:e.discriminatedUnion("kind",[e.object({kind:e.literal("file"),path:e.string().min(1)}).strict(),e.object({kind:e.literal("env"),name:e.string().min(1)}).strict()])}).strict()).optional(),dockerfileChecks:e.array(e.string().min(1)).optional(),provenance:e.boolean(),sbom:e.boolean(),push:e.boolean(),load:e.boolean(),pushByDigest:e.boolean().optional(),imageName:e.string().min(1).optional(),builder:e.string().min(1,"Builder name cannot be empty").optional(),metadataFile:e.string().min(1)}).strict().refine(t=>!(t.platforms.length>1&&t.load),{message:"Multi-arch builds cannot use load:true (Docker limitation); use push:true or omit both for cache-only"}).refine(t=>!(t.push&&t.load),{message:"push and load are mutually exclusive"}).refine(t=>t.pushByDigest!==!0||t.imageName!==void 0,{message:"pushByDigest requires imageName (the registry repo to push to)"}).refine(t=>t.pushByDigest===!0||t.tags.length>=1,{message:"tags must be non-empty unless pushByDigest is set"}),o=e.object({digest:e.string(),imageDigests:e.record(e.string(),e.string()),metadata:e.record(e.string(),e.unknown()),platforms:e.array(e.string()),cacheHits:e.number().int().nonnegative(),cacheMisses:e.number().int().nonnegative()}).strict(),l=e.object({kind:i,message:e.string(),stderrTail:e.array(e.string()).optional(),details:e.unknown().optional()}).strict();export{s as BuildxBuildArgsSchema,o as BuildxBuildResultSchema,i as DockerCliErrorKindSchema,l as DockerCliErrorSchema,r as isDockerCliErrorKind};
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Ephemeral, isolated Docker credential context for ECR pushes.
3
+ *
4
+ * Why this exists. `docker login --password-stdin <ecr-registry>` delegates
5
+ * credential STORAGE to the configured `credsStore` credential helper. On
6
+ * macOS that helper is `docker-credential-osxkeychain`, which cannot overwrite
7
+ * a pre-existing keychain entry — it exits non-zero with
8
+ * `errSecDuplicateItem (-25299)` ("The specified item already exists in the
9
+ * keychain"). A single stale entry then makes EVERY `fjall deploy` fail at
10
+ * "Authenticating to ECR…" until the user manually runs
11
+ * `security delete-internet-password`. The same `docker login` is also a
12
+ * read-modify-write of the global `~/.docker/config.json`, which races and can
13
+ * corrupt under concurrent deploys.
14
+ *
15
+ * The fix. Fjall's own ECR auth must never depend on, mutate, or be corrupted
16
+ * by the user's OS keychain. `createEcrAuthSession` writes a throwaway,
17
+ * deploy-scoped `DOCKER_CONFIG` directory whose `config.json` carries the ECR
18
+ * token inline under `auths` and has NO `credsStore` — so docker and buildx
19
+ * read the token straight from the file and the keychain is never touched. The
20
+ * user's real environment is preserved where it matters:
21
+ * - docker contexts (the active daemon endpoint — e.g. OrbStack, colima, a
22
+ * remote TLS endpoint) are symlinked in, so a non-default `currentContext`
23
+ * still resolves;
24
+ * - the buildx builder store is pointed at the real one via `BUILDX_CONFIG`,
25
+ * so the persistent "fjall" builder is reused rather than re-created;
26
+ * - all non-credential config keys (proxies, HttpHeaders, currentContext,
27
+ * other registries' inline auths, per-registry credHelpers) are carried
28
+ * over.
29
+ * The session env (`DOCKER_CONFIG` + `BUILDX_CONFIG`) is applied to every
30
+ * docker subprocess for the deploy; `dispose()` removes the temp dir.
31
+ *
32
+ * Trade-off (documented, accepted). Dropping the global `credsStore` is
33
+ * unavoidable: docker ignores an inline `auths[...].auth` whenever a
34
+ * `credsStore` is configured, so the only way to honour our inline ECR token
35
+ * is to write a config without it. The cost is that a base image pulled from a
36
+ * NON-ECR private registry whose credentials live ONLY in the OS keychain
37
+ * (never inline, never via a per-registry credHelper) would lose them for this
38
+ * build. That is rare for Fjall apps (public base images) and is the correct
39
+ * exchange for fixing a hard, deploy-blocking failure that hits every macOS
40
+ * user with a stale keychain entry. ECR tokens are short-lived (~12h); the
41
+ * file is mode-0600 inside a mode-0700 temp dir and is removed after the push.
42
+ */
43
+ import type { DockerCliLogger } from "./DockerCli.js";
44
+ export interface EcrAuthSession {
45
+ /** Env vars to apply to every docker subprocess for the duration of the deploy. */
46
+ readonly env: Readonly<Record<string, string>>;
47
+ /** Remove the ephemeral config dir. Best-effort; never throws. */
48
+ dispose(): Promise<void>;
49
+ }
50
+ export interface CreateEcrAuthSessionParams {
51
+ /** ECR registry — bare host or `https://<host>`; normalised to a host key. */
52
+ readonly registry: string;
53
+ /** Always `AWS` for ECR. */
54
+ readonly username: string;
55
+ /** The decoded ECR authorization token (the password half of `AWS:<token>`). */
56
+ readonly password: string;
57
+ /** The env the DockerCli was constructed with — source of the real config dir. */
58
+ readonly baseEnv: NodeJS.ProcessEnv;
59
+ readonly logger: DockerCliLogger;
60
+ }
61
+ export declare function createEcrAuthSession(params: CreateEcrAuthSessionParams): Promise<EcrAuthSession>;
@@ -0,0 +1 @@
1
+ import{mkdtemp as S,readFile as v,rm as g,stat as F,symlink as _,writeFile as j}from"node:fs/promises";import{homedir as H,tmpdir as I}from"node:os";import{join as i}from"node:path";import{maskSensitiveOutput as N}from"../securityHelpers.js";import{DOCKER_CLI_LOG_CATEGORY as c}from"./dockerCliConstants.js";function m(e){return N(e instanceof Error?e.message:String(e))}function b(e){let r=e.replace(/^https?:\/\//i,"");const t=r.indexOf("/");return t!==-1&&(r=r.slice(0,t)),r}function G(e){const r=e.DOCKER_CONFIG;if(r!==void 0&&r!=="")return r;const t=e.HOME!==void 0&&e.HOME!==""?e.HOME:H();return i(t,".docker")}function A(e,r){const t=e.BUILDX_CONFIG;return t!==void 0&&t!==""?t:i(r,"buildx")}async function B(e,r){let t;try{t=await v(i(e,"config.json"),"utf8")}catch{return{}}try{const n=JSON.parse(t);return n===null||typeof n!="object"||Array.isArray(n)?{}:n}catch(n){return r.warn(c,"Real docker config.json is not valid JSON; using a minimal ephemeral config",{error:m(n)}),{}}}async function L(e,r,t){const n=i(e,"contexts");try{if(!(await F(n)).isDirectory())return}catch{return}try{await _(n,i(r,"contexts"),"dir")}catch(a){t.warn(c,"Could not link docker contexts into the ephemeral config; a non-default docker context may not resolve for this build",{error:m(a)})}}async function $(e){const{registry:r,username:t,password:n,baseEnv:a,logger:s}=e,l=b(r),u=G(a),p=A(a,u),C=await B(u,s),{credsStore:y,credHelpers:O,auths:w,...x}=C,f={...O??{}};delete f[l];const D=Buffer.from(`${t}:${n}`,"utf8").toString("base64"),k={...w??{},[l]:{auth:D}},E={...x,auths:k,...Object.keys(f).length>0?{credHelpers:f}:{}};let d;try{const o=await S(i(I(),"fjall-ecr-auth-"));d=o,await j(i(o,"config.json"),JSON.stringify(E),{mode:384}),await L(u,o,s),s.debug(c,"Established ephemeral ECR credential context",{registry:l,dockerConfig:o,buildxConfig:p});let h=!1;return{env:{DOCKER_CONFIG:o,BUILDX_CONFIG:p},async dispose(){h||(h=!0,await g(o,{recursive:!0,force:!0}).catch(R=>{s.warn(c,"Failed to remove ephemeral ECR credential dir",{path:o,error:m(R)})}))}}}catch(o){throw d!==void 0&&await g(d,{recursive:!0,force:!0}).catch(()=>{}),o}}export{$ as createEcrAuthSession};
@@ -2,9 +2,12 @@ export { type Result, isSuccess, isFailure, success, failure } from "./result.js
2
2
  export { DOCKER_CLI_LOG_CATEGORY, DOCKER_CLI_BUILDX_LOG_CATEGORY, BUILDX_VERSION_FLOOR, ENGINE_VERSION_FLOOR, PrerequisiteMissingExitCode, DEFAULT_BUILDER_NAME, DEFAULT_DOCKER_BIN, SIGTERM_GRACE_MS, STDERR_TAIL_LINES, DEFAULT_BUILD_TIMEOUT_MS, DEFAULT_PUSH_TIMEOUT_MS, DEFAULT_PULL_TIMEOUT_MS, DEFAULT_INSPECT_TIMEOUT_MS, DEFAULT_DAEMON_PROBE_TIMEOUT_MS } from "./dockerCliConstants.js";
3
3
  export { BuildxBuildArgsSchema, BuildxBuildResultSchema, DockerCliErrorKindSchema, DockerCliErrorSchema, isDockerCliErrorKind, type BuildxBuildArgs, type BuildxBuildResult, type DockerCliError, type DockerCliErrorKind } from "./dockerCliSchemas.js";
4
4
  export { buildxArgvBuilder } from "./buildxArgvBuilder.js";
5
+ export { evaluateBakeGuard, evaluateResolvedBuildArgValues, acknowledgedBuildArgKeys, BAKE_GUARD_EXPOSURE_CLAUSE, type BakeGuardFinding, type BakeGuardFindingReason, type BakeGuardWarning, type BakeGuardResult } from "./bakeGuard.js";
6
+ export { PUBLIC_BUILD_ARG_PREFIXES, isPublicBuildVarName, inferPublicBuildArgKeys, type InferPublicBuildArgKeysInput } from "./buildArgInference.js";
5
7
  export { parseRawjsonLine, type RawjsonEnvelope, type RawjsonVertex, type RawjsonStatus, type RawjsonLog, type RawjsonWarning } from "./rawjsonParser.js";
6
8
  export { rawjsonToVertexEvent, type RawjsonVertexEvent, type NormalisedVertex, type NormalisedStatus, type NormalisedLog, type NormalisedWarning } from "./rawjsonToVertexEvent.js";
7
9
  export { parseMetadataFile } from "./metadataFileParser.js";
8
10
  export { projectBuildxResult, type DockerBuildResultLike, type ProjectBuildxResultArgs } from "./projectBuildxResult.js";
9
11
  export { abortChildProcess } from "./abortHelpers.js";
10
12
  export { DockerCli, type DockerCliLogger, type DockerCliOptions, type BuildxProgressEvent, type PushProgressEvent, type PushResult, type PullProgressEvent, type PullResult, type ImagetoolsInspect, type EcrLoginArgs, type BuildxCapabilities, type DaemonInfo } from "./DockerCli.js";
13
+ export { createEcrAuthSession, type EcrAuthSession, type CreateEcrAuthSessionParams } from "./ecrCredentialStore.js";
@@ -1 +1 @@
1
- import{isSuccess as E,isFailure as _,success as o,failure as i}from"./result.js";import{DOCKER_CLI_LOG_CATEGORY as L,DOCKER_CLI_BUILDX_LOG_CATEGORY as s,BUILDX_VERSION_FLOOR as t,ENGINE_VERSION_FLOOR as D,PrerequisiteMissingExitCode as O,DEFAULT_BUILDER_NAME as I,DEFAULT_DOCKER_BIN as R,SIGTERM_GRACE_MS as S,STDERR_TAIL_LINES as U,DEFAULT_BUILD_TIMEOUT_MS as l,DEFAULT_PUSH_TIMEOUT_MS as x,DEFAULT_PULL_TIMEOUT_MS as M,DEFAULT_INSPECT_TIMEOUT_MS as A,DEFAULT_DAEMON_PROBE_TIMEOUT_MS as C}from"./dockerCliConstants.js";import{BuildxBuildArgsSchema as c,BuildxBuildResultSchema as m,DockerCliErrorKindSchema as u,DockerCliErrorSchema as p,isDockerCliErrorKind as d}from"./dockerCliSchemas.js";import{buildxArgvBuilder as f}from"./buildxArgvBuilder.js";import{parseRawjsonLine as N}from"./rawjsonParser.js";import{rawjsonToVertexEvent as G}from"./rawjsonToVertexEvent.js";import{parseMetadataFile as h}from"./metadataFileParser.js";import{projectBuildxResult as k}from"./projectBuildxResult.js";import{abortChildProcess as j}from"./abortHelpers.js";import{DockerCli as b}from"./DockerCli.js";export{t as BUILDX_VERSION_FLOOR,c as BuildxBuildArgsSchema,m as BuildxBuildResultSchema,I as DEFAULT_BUILDER_NAME,l as DEFAULT_BUILD_TIMEOUT_MS,C as DEFAULT_DAEMON_PROBE_TIMEOUT_MS,R as DEFAULT_DOCKER_BIN,A as DEFAULT_INSPECT_TIMEOUT_MS,M as DEFAULT_PULL_TIMEOUT_MS,x as DEFAULT_PUSH_TIMEOUT_MS,s as DOCKER_CLI_BUILDX_LOG_CATEGORY,L as DOCKER_CLI_LOG_CATEGORY,b as DockerCli,u as DockerCliErrorKindSchema,p as DockerCliErrorSchema,D as ENGINE_VERSION_FLOOR,O as PrerequisiteMissingExitCode,S as SIGTERM_GRACE_MS,U as STDERR_TAIL_LINES,j as abortChildProcess,f as buildxArgvBuilder,i as failure,d as isDockerCliErrorKind,_ as isFailure,E as isSuccess,h as parseMetadataFile,N as parseRawjsonLine,k as projectBuildxResult,G as rawjsonToVertexEvent,o as success};
1
+ import{isSuccess as o,isFailure as E,success as _,failure as i}from"./result.js";import{DOCKER_CLI_LOG_CATEGORY as s,DOCKER_CLI_BUILDX_LOG_CATEGORY as t,BUILDX_VERSION_FLOOR as a,ENGINE_VERSION_FLOOR as u,PrerequisiteMissingExitCode as L,DEFAULT_BUILDER_NAME as T,DEFAULT_DOCKER_BIN as A,SIGTERM_GRACE_MS as R,STDERR_TAIL_LINES as U,DEFAULT_BUILD_TIMEOUT_MS as D,DEFAULT_PUSH_TIMEOUT_MS as I,DEFAULT_PULL_TIMEOUT_MS as S,DEFAULT_INSPECT_TIMEOUT_MS as O,DEFAULT_DAEMON_PROBE_TIMEOUT_MS as d}from"./dockerCliConstants.js";import{BuildxBuildArgsSchema as c,BuildxBuildResultSchema as x,DockerCliErrorKindSchema as m,DockerCliErrorSchema as C,isDockerCliErrorKind as p}from"./dockerCliSchemas.js";import{buildxArgvBuilder as f}from"./buildxArgvBuilder.js";import{evaluateBakeGuard as P,evaluateResolvedBuildArgValues as n,acknowledgedBuildArgKeys as G,BAKE_GUARD_EXPOSURE_CLAUSE as N}from"./bakeGuard.js";import{PUBLIC_BUILD_ARG_PREFIXES as g,isPublicBuildVarName as h,inferPublicBuildArgKeys as k}from"./buildArgInference.js";import{parseRawjsonLine as V}from"./rawjsonParser.js";import{rawjsonToVertexEvent as X}from"./rawjsonToVertexEvent.js";import{parseMetadataFile as w}from"./metadataFileParser.js";import{projectBuildxResult as Y}from"./projectBuildxResult.js";import{abortChildProcess as H}from"./abortHelpers.js";import{DockerCli as J}from"./DockerCli.js";import{createEcrAuthSession as W}from"./ecrCredentialStore.js";export{N as BAKE_GUARD_EXPOSURE_CLAUSE,a as BUILDX_VERSION_FLOOR,c as BuildxBuildArgsSchema,x as BuildxBuildResultSchema,T as DEFAULT_BUILDER_NAME,D as DEFAULT_BUILD_TIMEOUT_MS,d as DEFAULT_DAEMON_PROBE_TIMEOUT_MS,A as DEFAULT_DOCKER_BIN,O as DEFAULT_INSPECT_TIMEOUT_MS,S as DEFAULT_PULL_TIMEOUT_MS,I as DEFAULT_PUSH_TIMEOUT_MS,t as DOCKER_CLI_BUILDX_LOG_CATEGORY,s as DOCKER_CLI_LOG_CATEGORY,J as DockerCli,m as DockerCliErrorKindSchema,C as DockerCliErrorSchema,u as ENGINE_VERSION_FLOOR,g as PUBLIC_BUILD_ARG_PREFIXES,L as PrerequisiteMissingExitCode,R as SIGTERM_GRACE_MS,U as STDERR_TAIL_LINES,H as abortChildProcess,G as acknowledgedBuildArgKeys,f as buildxArgvBuilder,W as createEcrAuthSession,P as evaluateBakeGuard,n as evaluateResolvedBuildArgValues,i as failure,k as inferPublicBuildArgKeys,p as isDockerCliErrorKind,E as isFailure,h as isPublicBuildVarName,o as isSuccess,w as parseMetadataFile,V as parseRawjsonLine,Y as projectBuildxResult,X as rawjsonToVertexEvent,_ as success};
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { DNS_APEX, getDomainExportNames, type ManagedDomainExports } from "./infra/domainExports.js";
2
2
  export { BACKUP_VAULT_NAME } from "./infra/backupVault.js";
3
+ export { APPROVAL_TOKEN_OUTPUT_PREFIX } from "./deploy/approvalTokenOutput.js";
3
4
  export { imageTagParameterName } from "./infra/imageTags.js";
4
5
  export { toPascalCase, toKebab, toValidDatabaseName, toScreamingSnake, capitalise, getSafeZoneName, accountConstructKey, hasAsciiStableConstructKey } from "./naming/caseConversion.js";
5
6
  export { findAccountNameCollision, type AccountNameCollision } from "./naming/accountNameCollision.js";
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{DNS_APEX as o,getDomainExportNames as t}from"./infra/domainExports.js";import{BACKUP_VAULT_NAME as E}from"./infra/backupVault.js";import{imageTagParameterName as i}from"./infra/imageTags.js";import{toPascalCase as _,toKebab as A,toValidDatabaseName as m,toScreamingSnake as s,capitalise as R,getSafeZoneName as C,accountConstructKey as N,hasAsciiStableConstructKey as T}from"./naming/caseConversion.js";import{findAccountNameCollision as g}from"./naming/accountNameCollision.js";import{defaultConnectedAccountName as c,suffixedAccountName as O,REGION_SHORT_CODES as I,findTrailingRegionShortCode as x,regionSuffixRejectionMessage as u}from"./naming/connectedAccountName.js";import{normaliseError as l,getErrorMessage as M,hasErrorCode as P,getErrorCode as D,getErrorStack as V,formatErrorString as U}from"./errorUtils.js";import{singleton as h}from"./async/singleton.js";import{DANGEROUS_ENV_VARS as H,filterDangerousEnvVars as L,maskSensitiveOutput as b,parseShellArgs as F}from"./securityHelpers.js";import{sleep as y}from"./async/sleep.js";import{mapSettledWithConcurrency as X}from"./async/concurrency.js";import{ACCOUNT_STAGES_WITH_ROOT as B,STRUCTURAL_ENVIRONMENTS as j,ACCOUNT_STAGES as z,ACCOUNT_STAGE_LABELS as Y,isAccountStage as Z,ACCOUNT_TIERS as q,AccountTierSchema as w,isAccountTier as J,environmentToTier as Q,stageFromWireEnvironment as $,accountTier as ee,getEnvironmentLabel as re,ACCOUNT_ROLES as oe}from"./environments.js";import{RESOURCE_CATEGORIES as ne,categoriseResource as Ee,getExpectedDuration as ae,getFriendlyResourceType as ie}from"./resourceCategorisation.js";import{parseGitRemoteUrl as _e}from"./repo/gitRemoteParser.js";import{abbreviateRegion as me,AWS_REGIONS_METADATA as se,DEFAULT_REGION as Re,getRegionInfo as Ce,MAX_SECONDARY_REGIONS as Ne,OPT_IN_REGION_CODES as Te,optInRegionWarning as fe,regions as ge,suggestRegionForTimezone as pe}from"./infra/regions.js";import{SCOPE_VALUES as Oe}from"./infra/tokenScopes.js";import{ConnectionWireSchema as xe,ConnectionsListResponseSchema as ue}from"./infra/connectionsWire.js";import{deriveRegionsFromOrgConfig as le,deriveTargets as Me,deriveAllTargets as Pe,environmentOrTier as De,findTarget as Ve,generateTargetName as Ue}from"./targets.js";import{buildAppConfigPath as he}from"./repo/appPath.js";import{findInfrastructurePaths as He,findBoundaryPath as Le,isInfrastructureFile as be}from"./repo/findInfrastructurePaths.js";import{inferContainerFromCandidates as We}from"./repo/inferContainerFromCandidates.js";import{RESERVED_APP_NAMES as Ke,RESERVED_APP_NAME_MESSAGE as Xe,isReservedAppName as ke}from"./naming/reservedAppNames.js";import{deriveContentHashTag as je}from"./infra/deriveContentHashTag.js";import{MIGRATION_SNAPSHOT_NAME_PREFIX as Ye,EXPECTED_SCHEMA_VERSION_ENV as Ze,EXPECTED_SCHEMA_VERSION_TOOL_ENV as qe,EXPECTED_CH_SCHEMA_VERSION_ENV as we,SCHEMA_ADMIN_USER_ENV as Je,SCHEMA_ADMIN_PASSWORD_ENV as Qe,PRISMA_MIGRATION_DIR_RE as $e,CLICKHOUSE_MIGRATION_SKIP_RE as er}from"./migration/constants.js";export{oe as ACCOUNT_ROLES,z as ACCOUNT_STAGES,B as ACCOUNT_STAGES_WITH_ROOT,Y as ACCOUNT_STAGE_LABELS,q as ACCOUNT_TIERS,se as AWS_REGIONS_METADATA,w as AccountTierSchema,E as BACKUP_VAULT_NAME,er as CLICKHOUSE_MIGRATION_SKIP_RE,xe as ConnectionWireSchema,ue as ConnectionsListResponseSchema,H as DANGEROUS_ENV_VARS,Re as DEFAULT_REGION,o as DNS_APEX,we as EXPECTED_CH_SCHEMA_VERSION_ENV,Ze as EXPECTED_SCHEMA_VERSION_ENV,qe as EXPECTED_SCHEMA_VERSION_TOOL_ENV,Ne as MAX_SECONDARY_REGIONS,Ye as MIGRATION_SNAPSHOT_NAME_PREFIX,Te as OPT_IN_REGION_CODES,$e as PRISMA_MIGRATION_DIR_RE,I as REGION_SHORT_CODES,Ke as RESERVED_APP_NAMES,Xe as RESERVED_APP_NAME_MESSAGE,ne as RESOURCE_CATEGORIES,Qe as SCHEMA_ADMIN_PASSWORD_ENV,Je as SCHEMA_ADMIN_USER_ENV,Oe as SCOPE_VALUES,j as STRUCTURAL_ENVIRONMENTS,me as abbreviateRegion,N as accountConstructKey,ee as accountTier,he as buildAppConfigPath,R as capitalise,Ee as categoriseResource,c as defaultConnectedAccountName,Pe as deriveAllTargets,je as deriveContentHashTag,le as deriveRegionsFromOrgConfig,Me as deriveTargets,De as environmentOrTier,Q as environmentToTier,L as filterDangerousEnvVars,g as findAccountNameCollision,Le as findBoundaryPath,He as findInfrastructurePaths,Ve as findTarget,x as findTrailingRegionShortCode,U as formatErrorString,Ue as generateTargetName,t as getDomainExportNames,re as getEnvironmentLabel,D as getErrorCode,M as getErrorMessage,V as getErrorStack,ae as getExpectedDuration,ie as getFriendlyResourceType,Ce as getRegionInfo,C as getSafeZoneName,T as hasAsciiStableConstructKey,P as hasErrorCode,i as imageTagParameterName,We as inferContainerFromCandidates,Z as isAccountStage,J as isAccountTier,be as isInfrastructureFile,ke as isReservedAppName,X as mapSettledWithConcurrency,b as maskSensitiveOutput,l as normaliseError,fe as optInRegionWarning,_e as parseGitRemoteUrl,F as parseShellArgs,u as regionSuffixRejectionMessage,ge as regions,h as singleton,y as sleep,$ as stageFromWireEnvironment,O as suffixedAccountName,pe as suggestRegionForTimezone,A as toKebab,_ as toPascalCase,s as toScreamingSnake,m as toValidDatabaseName};
1
+ import{DNS_APEX as o,getDomainExportNames as t}from"./infra/domainExports.js";import{BACKUP_VAULT_NAME as E}from"./infra/backupVault.js";import{APPROVAL_TOKEN_OUTPUT_PREFIX as i}from"./deploy/approvalTokenOutput.js";import{imageTagParameterName as _}from"./infra/imageTags.js";import{toPascalCase as m,toKebab as s,toValidDatabaseName as R,toScreamingSnake as T,capitalise as N,getSafeZoneName as C,accountConstructKey as f,hasAsciiStableConstructKey as g}from"./naming/caseConversion.js";import{findAccountNameCollision as O}from"./naming/accountNameCollision.js";import{defaultConnectedAccountName as I,suffixedAccountName as x,REGION_SHORT_CODES as u,findTrailingRegionShortCode as P,regionSuffixRejectionMessage as d}from"./naming/connectedAccountName.js";import{normaliseError as M,getErrorMessage as D,hasErrorCode as U,getErrorCode as V,getErrorStack as G,formatErrorString as h}from"./errorUtils.js";import{singleton as L}from"./async/singleton.js";import{DANGEROUS_ENV_VARS as F,filterDangerousEnvVars as b,maskSensitiveOutput as K,parseShellArgs as W}from"./securityHelpers.js";import{sleep as y}from"./async/sleep.js";import{mapSettledWithConcurrency as B}from"./async/concurrency.js";import{ACCOUNT_STAGES_WITH_ROOT as z,STRUCTURAL_ENVIRONMENTS as Y,ACCOUNT_STAGES as Z,ACCOUNT_STAGE_LABELS as q,isAccountStage as w,ACCOUNT_TIERS as J,AccountTierSchema as Q,isAccountTier as $,environmentToTier as ee,stageFromWireEnvironment as re,accountTier as oe,getEnvironmentLabel as te,ACCOUNT_ROLES as ne}from"./environments.js";import{RESOURCE_CATEGORIES as ae,categoriseResource as ie,getExpectedDuration as Se,getFriendlyResourceType as _e}from"./resourceCategorisation.js";import{parseGitRemoteUrl as me}from"./repo/gitRemoteParser.js";import{abbreviateRegion as Re,AWS_REGIONS_METADATA as Te,DEFAULT_REGION as Ne,getRegionInfo as Ce,MAX_SECONDARY_REGIONS as fe,OPT_IN_REGION_CODES as ge,optInRegionWarning as pe,regions as Oe,suggestRegionForTimezone as ce}from"./infra/regions.js";import{SCOPE_VALUES as xe}from"./infra/tokenScopes.js";import{ConnectionWireSchema as Pe,ConnectionsListResponseSchema as de}from"./infra/connectionsWire.js";import{deriveRegionsFromOrgConfig as Me,deriveTargets as De,deriveAllTargets as Ue,environmentOrTier as Ve,findTarget as Ge,generateTargetName as he}from"./targets.js";import{buildAppConfigPath as Le}from"./repo/appPath.js";import{findInfrastructurePaths as Fe,findBoundaryPath as be,isInfrastructureFile as Ke}from"./repo/findInfrastructurePaths.js";import{inferContainerFromCandidates as Xe}from"./repo/inferContainerFromCandidates.js";import{RESERVED_APP_NAMES as ke,RESERVED_APP_NAME_MESSAGE as Be,isReservedAppName as je}from"./naming/reservedAppNames.js";import{deriveContentHashTag as Ye}from"./infra/deriveContentHashTag.js";import{MIGRATION_SNAPSHOT_NAME_PREFIX as qe,EXPECTED_SCHEMA_VERSION_ENV as we,EXPECTED_SCHEMA_VERSION_TOOL_ENV as Je,EXPECTED_CH_SCHEMA_VERSION_ENV as Qe,SCHEMA_ADMIN_USER_ENV as $e,SCHEMA_ADMIN_PASSWORD_ENV as er,PRISMA_MIGRATION_DIR_RE as rr,CLICKHOUSE_MIGRATION_SKIP_RE as or}from"./migration/constants.js";export{ne as ACCOUNT_ROLES,Z as ACCOUNT_STAGES,z as ACCOUNT_STAGES_WITH_ROOT,q as ACCOUNT_STAGE_LABELS,J as ACCOUNT_TIERS,i as APPROVAL_TOKEN_OUTPUT_PREFIX,Te as AWS_REGIONS_METADATA,Q as AccountTierSchema,E as BACKUP_VAULT_NAME,or as CLICKHOUSE_MIGRATION_SKIP_RE,Pe as ConnectionWireSchema,de as ConnectionsListResponseSchema,F as DANGEROUS_ENV_VARS,Ne as DEFAULT_REGION,o as DNS_APEX,Qe as EXPECTED_CH_SCHEMA_VERSION_ENV,we as EXPECTED_SCHEMA_VERSION_ENV,Je as EXPECTED_SCHEMA_VERSION_TOOL_ENV,fe as MAX_SECONDARY_REGIONS,qe as MIGRATION_SNAPSHOT_NAME_PREFIX,ge as OPT_IN_REGION_CODES,rr as PRISMA_MIGRATION_DIR_RE,u as REGION_SHORT_CODES,ke as RESERVED_APP_NAMES,Be as RESERVED_APP_NAME_MESSAGE,ae as RESOURCE_CATEGORIES,er as SCHEMA_ADMIN_PASSWORD_ENV,$e as SCHEMA_ADMIN_USER_ENV,xe as SCOPE_VALUES,Y as STRUCTURAL_ENVIRONMENTS,Re as abbreviateRegion,f as accountConstructKey,oe as accountTier,Le as buildAppConfigPath,N as capitalise,ie as categoriseResource,I as defaultConnectedAccountName,Ue as deriveAllTargets,Ye as deriveContentHashTag,Me as deriveRegionsFromOrgConfig,De as deriveTargets,Ve as environmentOrTier,ee as environmentToTier,b as filterDangerousEnvVars,O as findAccountNameCollision,be as findBoundaryPath,Fe as findInfrastructurePaths,Ge as findTarget,P as findTrailingRegionShortCode,h as formatErrorString,he as generateTargetName,t as getDomainExportNames,te as getEnvironmentLabel,V as getErrorCode,D as getErrorMessage,G as getErrorStack,Se as getExpectedDuration,_e as getFriendlyResourceType,Ce as getRegionInfo,C as getSafeZoneName,g as hasAsciiStableConstructKey,U as hasErrorCode,_ as imageTagParameterName,Xe as inferContainerFromCandidates,w as isAccountStage,$ as isAccountTier,Ke as isInfrastructureFile,je as isReservedAppName,B as mapSettledWithConcurrency,K as maskSensitiveOutput,M as normaliseError,pe as optInRegionWarning,me as parseGitRemoteUrl,W as parseShellArgs,d as regionSuffixRejectionMessage,Oe as regions,L as singleton,y as sleep,re as stageFromWireEnvironment,x as suffixedAccountName,ce as suggestRegionForTimezone,s as toKebab,m as toPascalCase,T as toScreamingSnake,R as toValidDatabaseName};
@@ -6,5 +6,5 @@
6
6
  * pure CDK synth path) MUST import directly from `./schemas` instead of
7
7
  * the barrel — the package.json `exports` map exposes both subpaths.
8
8
  */
9
- export { DockerBuildSchema, DockerBuildPartialSchema, mergeDockerBuild, ManifestServiceSchema, ManifestPatternSchema, ManifestEcrSchema, ManifestLambdaSchema, ManifestStackHashSchema, ResourceMapEntrySchema, FjallManifestSchema, FJALL_MANIFEST_FILENAME, MANIFEST_SCHEMA_VERSION, type DockerBuild, type DockerBuildPartial, type ManifestService, type ManifestPattern, type ManifestEcr, type ManifestLambda, type ManifestStackHash, type ResourceMapEntry, type FjallManifest } from "./schemas.js";
9
+ export { DockerBuildSchema, DockerBuildArgValueSchema, DockerBuildSecretRefSchema, DockerBuildPartialSchema, mergeDockerBuild, ManifestServiceSchema, ManifestPatternSchema, ManifestEcrSchema, ManifestLambdaSchema, ManifestStackHashSchema, ResourceMapEntrySchema, FjallManifestSchema, FJALL_MANIFEST_FILENAME, MANIFEST_SCHEMA_VERSION, BUILDKIT_SECRET_ID_PATTERN, type DockerBuild, type DockerBuildArgValue, type DockerBuildSecretRef, type DockerBuildPartial, type ManifestService, type ManifestPattern, type ManifestEcr, type ManifestLambda, type ManifestStackHash, type ResourceMapEntry, type FjallManifest } from "./schemas.js";
10
10
  export { getManifestFilePath, readManifestFile, writeManifestFile, createEmptyManifest, readConstructMap, parseDockerServicesFromManifest, type ManifestDockerService } from "./io.js";
@@ -1 +1 @@
1
- import{DockerBuildSchema as t,DockerBuildPartialSchema as r,mergeDockerBuild as i,ManifestServiceSchema as c,ManifestPatternSchema as M,ManifestEcrSchema as s,ManifestLambdaSchema as S,ManifestStackHashSchema as m,ResourceMapEntrySchema as n,FjallManifestSchema as f,FJALL_MANIFEST_FILENAME as h,MANIFEST_SCHEMA_VERSION as o}from"./schemas.js";import{getManifestFilePath as E,readManifestFile as F,writeManifestFile as d,createEmptyManifest as p,readConstructMap as k,parseDockerServicesFromManifest as u}from"./io.js";export{r as DockerBuildPartialSchema,t as DockerBuildSchema,h as FJALL_MANIFEST_FILENAME,f as FjallManifestSchema,o as MANIFEST_SCHEMA_VERSION,s as ManifestEcrSchema,S as ManifestLambdaSchema,M as ManifestPatternSchema,c as ManifestServiceSchema,m as ManifestStackHashSchema,n as ResourceMapEntrySchema,p as createEmptyManifest,E as getManifestFilePath,i as mergeDockerBuild,u as parseDockerServicesFromManifest,k as readConstructMap,F as readManifestFile,d as writeManifestFile};
1
+ import{DockerBuildSchema as r,DockerBuildArgValueSchema as t,DockerBuildSecretRefSchema as c,DockerBuildPartialSchema as i,mergeDockerBuild as S,ManifestServiceSchema as m,ManifestPatternSchema as M,ManifestEcrSchema as s,ManifestLambdaSchema as f,ManifestStackHashSchema as n,ResourceMapEntrySchema as h,FjallManifestSchema as o,FJALL_MANIFEST_FILENAME as l,MANIFEST_SCHEMA_VERSION as E,BUILDKIT_SECRET_ID_PATTERN as F}from"./schemas.js";import{getManifestFilePath as u,readManifestFile as D,writeManifestFile as k,createEmptyManifest as A,readConstructMap as I,parseDockerServicesFromManifest as _}from"./io.js";export{F as BUILDKIT_SECRET_ID_PATTERN,t as DockerBuildArgValueSchema,i as DockerBuildPartialSchema,r as DockerBuildSchema,c as DockerBuildSecretRefSchema,l as FJALL_MANIFEST_FILENAME,o as FjallManifestSchema,E as MANIFEST_SCHEMA_VERSION,s as ManifestEcrSchema,f as ManifestLambdaSchema,M as ManifestPatternSchema,m as ManifestServiceSchema,n as ManifestStackHashSchema,h as ResourceMapEntrySchema,A as createEmptyManifest,u as getManifestFilePath,S as mergeDockerBuild,_ as parseDockerServicesFromManifest,I as readConstructMap,D as readManifestFile,k as writeManifestFile};
@@ -1 +1 @@
1
- import{readFile as l,writeFile as m,unlink as g,rename as h,mkdir as y}from"fs/promises";import{readFileSync as M}from"fs";import{dirname as F,join as f}from"path";import{logger as s}from"../logger.js";import{fileExists as S}from"../fsHelpers.js";import{getErrorMessage as d}from"../errorUtils.js";import{recordToConstructMap as b}from"../constructMap.js";import{DockerBuildSchema as w,FjallManifestSchema as A,FJALL_MANIFEST_FILENAME as u,MANIFEST_SCHEMA_VERSION as x}from"./schemas.js";function p(e){return f(e,u)}async function j(e){const t=p(e);if(!await S(t))return null;try{const n=await l(t,"utf-8"),r=JSON.parse(n),a=A.safeParse(r);return a.success||s.debug("FjallManifest","Manifest validation failed",{path:t,errors:a.error.issues.map(i=>`${i.path.join(".")}: ${i.message}`)}),a.success?a.data:null}catch(n){return s.debug("FjallManifest","Failed to read manifest file",{path:t,error:d(n)}),null}}async function _(e,t){const n=p(e),r=`${n}.${Date.now()}.tmp`;await y(F(n),{recursive:!0});try{await m(r,JSON.stringify(t,null,2),"utf-8"),await h(r,n)}catch(a){try{await g(r)}catch(i){s.debug("FjallManifest","Temp file cleanup failed (non-fatal)",{path:r,error:d(i)})}throw a}}function $(e){return{version:x,generatedAt:new Date().toISOString(),appName:e,services:[],lambdas:[],stacks:{}}}async function L(e){const t=await j(e);return t?.resourceMap?b(t.resourceMap):new Map}function o(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function k(e){return o(e)&&Object.values(e).every(t=>typeof t=="string")}function E(e){if(!o(e)||typeof e.path!="string"||e.path.length===0)return;const t=typeof e.context=="string"&&e.context.length>0?e.context:void 0,n=typeof e.target=="string"&&e.target.length>0?e.target:void 0,r=k(e.buildArgs)&&Object.keys(e.buildArgs).length>0?e.buildArgs:void 0,a={path:e.path,...t!==void 0&&{context:t},...n!==void 0&&{target:n},...r!==void 0&&{buildArgs:r}},i=w.safeParse(a);return i.success?i.data:void 0}function R(e){const t=f(e,u);let n;try{n=M(t,"utf-8")}catch{return s.debug("FjallManifest","Manifest file not readable \u2014 no Docker services extracted",{path:t}),[]}let r;try{r=JSON.parse(n)}catch{return s.debug("FjallManifest","Manifest is not valid JSON \u2014 no Docker services extracted",{path:t}),[]}if(!o(r)||!Array.isArray(r.services))return[];const a=[];for(const i of r.services){if(!o(i)||typeof i.name!="string")continue;const c=E(i.docker);c!==void 0&&a.push({name:i.name,docker:c})}return a}export{$ as createEmptyManifest,p as getManifestFilePath,R as parseDockerServicesFromManifest,L as readConstructMap,j as readManifestFile,_ as writeManifestFile};
1
+ import{readFile as l,writeFile as m,unlink as g,rename as h,mkdir as y}from"fs/promises";import{readFileSync as M}from"fs";import{dirname as F,join as f}from"path";import{logger as o}from"../logger.js";import{fileExists as S}from"../fsHelpers.js";import{getErrorMessage as d}from"../errorUtils.js";import{recordToConstructMap as b}from"../constructMap.js";import{DockerBuildSchema as A,FjallManifestSchema as w,FJALL_MANIFEST_FILENAME as u,MANIFEST_SCHEMA_VERSION as x}from"./schemas.js";function p(e){return f(e,u)}async function k(e){const t=p(e);if(!await S(t))return null;try{const n=await l(t,"utf-8"),r=JSON.parse(n),i=w.safeParse(r);return i.success||o.debug("FjallManifest","Manifest validation failed",{path:t,errors:i.error.issues.map(a=>`${a.path.join(".")}: ${a.message}`)}),i.success?i.data:null}catch(n){return o.debug("FjallManifest","Failed to read manifest file",{path:t,error:d(n)}),null}}async function T(e,t){const n=p(e),r=`${n}.${Date.now()}.tmp`;await y(F(n),{recursive:!0});try{await m(r,JSON.stringify(t,null,2),"utf-8"),await h(r,n)}catch(i){try{await g(r)}catch(a){o.debug("FjallManifest","Temp file cleanup failed (non-fatal)",{path:r,error:d(a)})}throw i}}function _(e){return{version:x,generatedAt:new Date().toISOString(),appName:e,services:[],lambdas:[],stacks:{}}}async function $(e){const t=await k(e);return t?.resourceMap?b(t.resourceMap):new Map}function c(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function j(e){if(!c(e)||typeof e.path!="string"||e.path.length===0)return;const t=typeof e.context=="string"&&e.context.length>0?e.context:void 0,n=typeof e.target=="string"&&e.target.length>0?e.target:void 0,r=c(e.buildArgs)&&Object.keys(e.buildArgs).length>0?e.buildArgs:void 0,i=Array.isArray(e.buildSecrets)&&e.buildSecrets.length>0?e.buildSecrets:void 0,a={path:e.path,...t!==void 0&&{context:t},...n!==void 0&&{target:n},...r!==void 0&&{buildArgs:r},...i!==void 0&&{buildSecrets:i}},s=A.safeParse(a);return s.success?s.data:void 0}function L(e){const t=f(e,u);let n;try{n=M(t,"utf-8")}catch{return o.debug("FjallManifest","Manifest file not readable \u2014 no Docker services extracted",{path:t}),[]}let r;try{r=JSON.parse(n)}catch{return o.debug("FjallManifest","Manifest is not valid JSON \u2014 no Docker services extracted",{path:t}),[]}if(!c(r)||!Array.isArray(r.services))return[];const i=[];for(const a of r.services){if(!c(a)||typeof a.name!="string")continue;const s=j(a.docker);s!==void 0&&i.push({name:a.name,docker:s})}return i}export{_ as createEmptyManifest,p as getManifestFilePath,L as parseDockerServicesFromManifest,$ as readConstructMap,k as readManifestFile,T as writeManifestFile};
@@ -22,23 +22,116 @@ import { z } from "zod";
22
22
  export declare const FJALL_MANIFEST_FILENAME = "fjall-manifest.json";
23
23
  /** Current manifest schema version. */
24
24
  export declare const MANIFEST_SCHEMA_VERSION: 1;
25
+ /**
26
+ * Allowed character set for a BuildKit secret id. Deliberately conservative —
27
+ * NO comma, equals, or whitespace, all of which would break the
28
+ * `--secret id=<id>,src=…` argv buildx receives (`buildxArgvBuilder` joins the
29
+ * id and source with a comma and an `=`). Pinned at parse time so a manifest
30
+ * can never declare an id that mangles the buildx argv downstream.
31
+ */
32
+ export declare const BUILDKIT_SECRET_ID_PATTERN: RegExp;
33
+ /**
34
+ * A single build-time SECRET reference.
35
+ *
36
+ * Carries a REFERENCE only (never the value): exactly one of `ssm`,
37
+ * `secretsManager`, or `env` identifies where deploy-core resolves the value
38
+ * just-in-time before the build. The resolved value is injected via a BuildKit
39
+ * `--secret` mount (tmpfs, scoped to one `RUN --mount=type=secret`), so — unlike
40
+ * `buildArgs` — it NEVER lands in `--build-arg`, an image layer, `docker history`,
41
+ * or the manifest JSON. `id` is the BuildKit secret id the Dockerfile mounts
42
+ * (`RUN --mount=type=secret,id=<id>`).
43
+ *
44
+ * - `ssm` — an SSM Parameter Store name (resolved with decryption)
45
+ * - `secretsManager` — a Secrets Manager secret by `name` XOR `arn`, with an
46
+ * optional `field` to extract one key from a JSON secret value
47
+ * - `env` — a build-host environment variable name (read at build time)
48
+ */
49
+ export declare const DockerBuildSecretRefSchema: z.ZodObject<{
50
+ id: z.ZodString;
51
+ ssm: z.ZodOptional<z.ZodString>;
52
+ secretsManager: z.ZodOptional<z.ZodObject<{
53
+ name: z.ZodOptional<z.ZodString>;
54
+ arn: z.ZodOptional<z.ZodString>;
55
+ field: z.ZodOptional<z.ZodString>;
56
+ }, z.core.$strict>>;
57
+ env: z.ZodOptional<z.ZodString>;
58
+ }, z.core.$strict>;
59
+ export type DockerBuildSecretRef = z.infer<typeof DockerBuildSecretRefSchema>;
60
+ /**
61
+ * A single `buildArgs` value.
62
+ *
63
+ * Backward-compatible: a plain `string` is the common case — a public literal
64
+ * baked verbatim via `--build-arg KEY=VALUE`. The object form is the
65
+ * "public-but-sensitive" escape hatch (a restricted Stripe publishable key, a
66
+ * URL-scoped Mapbox token, a Sentry DSN): the value MUST be inlined into the
67
+ * client bundle, yet committing it as a literal would publish it to git too, so
68
+ * it is sourced from an SSM/Secrets Manager reference (or a build-host env var)
69
+ * and resolved just-in-time. Such a value still lands in `docker history`,
70
+ * provenance/SBOM, and the `<app>-cache` mode=max ECR repo, so the object form
71
+ * carries `acknowledgePublic: true` to opt out of the bake-guard deliberately
72
+ * (see `decisions/2026-04-27` / the build-time-env design § C3).
73
+ *
74
+ * Exactly one source (`ssm`, `secretsManager`, or `env`) is required on the
75
+ * object form — the same one-source contract as `DockerBuildSecretRef`.
76
+ */
77
+ export declare const DockerBuildArgValueSchema: z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
78
+ ssm: z.ZodOptional<z.ZodString>;
79
+ secretsManager: z.ZodOptional<z.ZodObject<{
80
+ name: z.ZodOptional<z.ZodString>;
81
+ arn: z.ZodOptional<z.ZodString>;
82
+ field: z.ZodOptional<z.ZodString>;
83
+ }, z.core.$strict>>;
84
+ env: z.ZodOptional<z.ZodString>;
85
+ acknowledgePublic: z.ZodOptional<z.ZodBoolean>;
86
+ }, z.core.$strict>]>;
87
+ export type DockerBuildArgValue = z.infer<typeof DockerBuildArgValueSchema>;
25
88
  /**
26
89
  * The universal Docker-build role primitive.
27
90
  *
28
91
  * `path` is the Dockerfile path (absolute or relative to `context`); `context`
29
92
  * is the build context (defaults to the directory containing `path` when
30
93
  * absent — that default lives in the build orchestrator, not in the schema);
31
- * `target` selects a multi-stage Dockerfile target; `buildArgs` are forwarded
32
- * to `docker buildx build --build-arg KEY=VALUE` and are the only mechanism
33
- * by which Vite-style `import.meta.env.VITE_*` variables can be baked into
34
- * the production client bundle (the substitution happens at build time, not
35
- * runtime, so container ENV vars cannot reach the client).
94
+ * `target` selects a multi-stage Dockerfile target.
95
+ *
96
+ * Two build-time injection channels, with a hard public/secret boundary:
97
+ *
98
+ * - `buildArgs` PUBLIC, world-readable. Forwarded to
99
+ * `docker buildx build --build-arg KEY=VALUE`, baked into the image, and
100
+ * recorded in `docker history` + provenance. The only mechanism by which
101
+ * Vite-style `import.meta.env.VITE_*` variables reach the production client
102
+ * bundle (substitution at build time, so container ENV cannot reach the client).
103
+ * NEVER put a credential here — a plain-string value is the common case; the
104
+ * object form (`DockerBuildArgValue`) is the public-but-sensitive escape hatch
105
+ * that sources from an SSM/SM ref with `acknowledgePublic: true`.
106
+ * - `buildSecrets` — SECRET references resolved just-in-time and injected via
107
+ * BuildKit `--secret` mounts. The value never enters `--build-arg`, an image
108
+ * layer, `docker history`, provenance, or the manifest JSON — only the
109
+ * REFERENCE is serialised. Use for npm tokens, private-registry creds, etc.
36
110
  */
37
111
  export declare const DockerBuildSchema: z.ZodObject<{
38
112
  path: z.ZodString;
39
113
  context: z.ZodOptional<z.ZodString>;
40
114
  target: z.ZodOptional<z.ZodString>;
41
- buildArgs: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
115
+ buildArgs: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
116
+ ssm: z.ZodOptional<z.ZodString>;
117
+ secretsManager: z.ZodOptional<z.ZodObject<{
118
+ name: z.ZodOptional<z.ZodString>;
119
+ arn: z.ZodOptional<z.ZodString>;
120
+ field: z.ZodOptional<z.ZodString>;
121
+ }, z.core.$strict>>;
122
+ env: z.ZodOptional<z.ZodString>;
123
+ acknowledgePublic: z.ZodOptional<z.ZodBoolean>;
124
+ }, z.core.$strict>]>>>;
125
+ buildSecrets: z.ZodOptional<z.ZodArray<z.ZodObject<{
126
+ id: z.ZodString;
127
+ ssm: z.ZodOptional<z.ZodString>;
128
+ secretsManager: z.ZodOptional<z.ZodObject<{
129
+ name: z.ZodOptional<z.ZodString>;
130
+ arn: z.ZodOptional<z.ZodString>;
131
+ field: z.ZodOptional<z.ZodString>;
132
+ }, z.core.$strict>>;
133
+ env: z.ZodOptional<z.ZodString>;
134
+ }, z.core.$strict>>>;
42
135
  }, z.core.$strict>;
43
136
  export type DockerBuild = z.infer<typeof DockerBuildSchema>;
44
137
  /**
@@ -52,7 +145,26 @@ export declare const DockerBuildPartialSchema: z.ZodObject<{
52
145
  path: z.ZodOptional<z.ZodString>;
53
146
  context: z.ZodOptional<z.ZodOptional<z.ZodString>>;
54
147
  target: z.ZodOptional<z.ZodOptional<z.ZodString>>;
55
- buildArgs: z.ZodOptional<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>>;
148
+ buildArgs: z.ZodOptional<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
149
+ ssm: z.ZodOptional<z.ZodString>;
150
+ secretsManager: z.ZodOptional<z.ZodObject<{
151
+ name: z.ZodOptional<z.ZodString>;
152
+ arn: z.ZodOptional<z.ZodString>;
153
+ field: z.ZodOptional<z.ZodString>;
154
+ }, z.core.$strict>>;
155
+ env: z.ZodOptional<z.ZodString>;
156
+ acknowledgePublic: z.ZodOptional<z.ZodBoolean>;
157
+ }, z.core.$strict>]>>>>;
158
+ buildSecrets: z.ZodOptional<z.ZodOptional<z.ZodArray<z.ZodObject<{
159
+ id: z.ZodString;
160
+ ssm: z.ZodOptional<z.ZodString>;
161
+ secretsManager: z.ZodOptional<z.ZodObject<{
162
+ name: z.ZodOptional<z.ZodString>;
163
+ arn: z.ZodOptional<z.ZodString>;
164
+ field: z.ZodOptional<z.ZodString>;
165
+ }, z.core.$strict>>;
166
+ env: z.ZodOptional<z.ZodString>;
167
+ }, z.core.$strict>>>>;
56
168
  }, z.core.$strict>;
57
169
  export type DockerBuildPartial = z.infer<typeof DockerBuildPartialSchema>;
58
170
  /**
@@ -71,7 +183,26 @@ declare const ManifestServiceSchema: z.ZodObject<{
71
183
  path: z.ZodString;
72
184
  context: z.ZodOptional<z.ZodString>;
73
185
  target: z.ZodOptional<z.ZodString>;
74
- buildArgs: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
186
+ buildArgs: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
187
+ ssm: z.ZodOptional<z.ZodString>;
188
+ secretsManager: z.ZodOptional<z.ZodObject<{
189
+ name: z.ZodOptional<z.ZodString>;
190
+ arn: z.ZodOptional<z.ZodString>;
191
+ field: z.ZodOptional<z.ZodString>;
192
+ }, z.core.$strict>>;
193
+ env: z.ZodOptional<z.ZodString>;
194
+ acknowledgePublic: z.ZodOptional<z.ZodBoolean>;
195
+ }, z.core.$strict>]>>>;
196
+ buildSecrets: z.ZodOptional<z.ZodArray<z.ZodObject<{
197
+ id: z.ZodString;
198
+ ssm: z.ZodOptional<z.ZodString>;
199
+ secretsManager: z.ZodOptional<z.ZodObject<{
200
+ name: z.ZodOptional<z.ZodString>;
201
+ arn: z.ZodOptional<z.ZodString>;
202
+ field: z.ZodOptional<z.ZodString>;
203
+ }, z.core.$strict>>;
204
+ env: z.ZodOptional<z.ZodString>;
205
+ }, z.core.$strict>>>;
75
206
  }, z.core.$strict>>;
76
207
  containerPort: z.ZodOptional<z.ZodNumber>;
77
208
  secrets: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -130,7 +261,26 @@ export declare const FjallManifestSchema: z.ZodObject<{
130
261
  path: z.ZodString;
131
262
  context: z.ZodOptional<z.ZodString>;
132
263
  target: z.ZodOptional<z.ZodString>;
133
- buildArgs: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
264
+ buildArgs: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
265
+ ssm: z.ZodOptional<z.ZodString>;
266
+ secretsManager: z.ZodOptional<z.ZodObject<{
267
+ name: z.ZodOptional<z.ZodString>;
268
+ arn: z.ZodOptional<z.ZodString>;
269
+ field: z.ZodOptional<z.ZodString>;
270
+ }, z.core.$strict>>;
271
+ env: z.ZodOptional<z.ZodString>;
272
+ acknowledgePublic: z.ZodOptional<z.ZodBoolean>;
273
+ }, z.core.$strict>]>>>;
274
+ buildSecrets: z.ZodOptional<z.ZodArray<z.ZodObject<{
275
+ id: z.ZodString;
276
+ ssm: z.ZodOptional<z.ZodString>;
277
+ secretsManager: z.ZodOptional<z.ZodObject<{
278
+ name: z.ZodOptional<z.ZodString>;
279
+ arn: z.ZodOptional<z.ZodString>;
280
+ field: z.ZodOptional<z.ZodString>;
281
+ }, z.core.$strict>>;
282
+ env: z.ZodOptional<z.ZodString>;
283
+ }, z.core.$strict>>>;
134
284
  }, z.core.$strict>>;
135
285
  containerPort: z.ZodOptional<z.ZodNumber>;
136
286
  secrets: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -1 +1 @@
1
- import{z as t}from"zod";const h="fjall-manifest.json",d=1,s=t.object({path:t.string(),context:t.string().min(1,"context cannot be empty").optional(),target:t.string().min(1,"target cannot be empty").optional(),buildArgs:t.record(t.string(),t.string()).optional()}).strict(),S=s.partial();function b(e,r){if(r===void 0)return e;const n=r.context??e.context,a=r.target??e.target,o=r.buildArgs??e.buildArgs;return{path:r.path??e.path,...n!==void 0&&{context:n},...a!==void 0&&{target:a},...o!==void 0&&{buildArgs:o}}}const c=t.object({name:t.string(),clusterName:t.string().optional(),docker:s.optional(),containerPort:t.number().optional(),secrets:t.array(t.string()).optional(),ssmSecretsPath:t.string().optional(),importedSecretNames:t.array(t.string()).optional()}).strict(),i=t.object({type:t.enum(["payload"]),name:t.string(),source:t.string()}).strict(),p=t.object({repositoryName:t.string()}).strict(),g=t.object({name:t.string(),secrets:t.array(t.string()).optional(),ssmSecretsPath:t.string().optional(),importedSecretNames:t.array(t.string()).optional()}).strict(),m=t.object({templateHash:t.string(),synthTimestamp:t.string()}).strict(),l=t.object({constructPath:t.string().max(512),group:t.string().max(128),resourceType:t.string().max(256)}).strict(),x=t.object({version:t.literal(d),generatedAt:t.string(),appName:t.string(),services:t.array(c),lambdas:t.array(g),pattern:i.optional(),ecr:p.optional(),stacks:t.record(t.string(),m),resourceMap:t.record(t.string(),l).optional()}).strict();export{S as DockerBuildPartialSchema,s as DockerBuildSchema,h as FJALL_MANIFEST_FILENAME,x as FjallManifestSchema,d as MANIFEST_SCHEMA_VERSION,p as ManifestEcrSchema,g as ManifestLambdaSchema,i as ManifestPatternSchema,c as ManifestServiceSchema,m as ManifestStackHashSchema,l as ResourceMapEntrySchema,b as mergeDockerBuild};
1
+ import{z as e}from"zod";const h="fjall-manifest.json",u=1,b=/^[A-Za-z0-9_.-]+$/,f=e.object({id:e.string().min(1,"buildSecret id cannot be empty").regex(b,"buildSecret id may contain only letters, digits, '_', '.', and '-' (no comma, '=', or whitespace, which would break the buildx --secret argv)"),ssm:e.string().min(1,"ssm parameter name cannot be empty").optional(),secretsManager:e.object({name:e.string().min(1,"secret name cannot be empty").optional(),arn:e.string().min(1,"secret arn cannot be empty").optional(),field:e.string().min(1,"secret field cannot be empty").optional()}).strict().refine(t=>t.name===void 0!=(t.arn===void 0),"secretsManager requires exactly one of name or arn").optional(),env:e.string().min(1,"env variable name cannot be empty").optional()}).strict().superRefine((t,n)=>{[t.ssm,t.secretsManager,t.env].filter(r=>r!==void 0).length!==1&&n.addIssue({code:e.ZodIssueCode.custom,message:"buildSecret requires exactly one source (ssm, secretsManager, or env)"})}),y=e.union([e.string(),e.object({ssm:e.string().min(1,"ssm parameter name cannot be empty").optional(),secretsManager:e.object({name:e.string().min(1,"secret name cannot be empty").optional(),arn:e.string().min(1,"secret arn cannot be empty").optional(),field:e.string().min(1,"secret field cannot be empty").optional()}).strict().refine(t=>t.name===void 0!=(t.arn===void 0),"secretsManager requires exactly one of name or arn").optional(),env:e.string().min(1,"env variable name cannot be empty").optional(),acknowledgePublic:e.boolean().optional()}).strict().superRefine((t,n)=>{[t.ssm,t.secretsManager,t.env].filter(r=>r!==void 0).length!==1&&n.addIssue({code:e.ZodIssueCode.custom,message:"buildArg value requires exactly one source (ssm, secretsManager, or env)"})})]),i=e.object({path:e.string(),context:e.string().min(1,"context cannot be empty").optional(),target:e.string().min(1,"target cannot be empty").optional(),buildArgs:e.record(e.string(),y).optional(),buildSecrets:e.array(f).optional()}).strict(),x=i.partial();function M(t,n){if(n===void 0)return t;const a=n.context??t.context,r=n.target??t.target,s=n.buildArgs??t.buildArgs,o=n.buildSecrets??t.buildSecrets;return{path:n.path??t.path,...a!==void 0&&{context:a},...r!==void 0&&{target:r},...s!==void 0&&{buildArgs:s},...o!==void 0&&{buildSecrets:o}}}const c=e.object({name:e.string(),clusterName:e.string().optional(),docker:i.optional(),containerPort:e.number().optional(),secrets:e.array(e.string()).optional(),ssmSecretsPath:e.string().optional(),importedSecretNames:e.array(e.string()).optional()}).strict(),m=e.object({type:e.enum(["payload"]),name:e.string(),source:e.string()}).strict(),l=e.object({repositoryName:e.string()}).strict(),p=e.object({name:e.string(),secrets:e.array(e.string()).optional(),ssmSecretsPath:e.string().optional(),importedSecretNames:e.array(e.string()).optional()}).strict(),d=e.object({templateHash:e.string(),synthTimestamp:e.string()}).strict(),g=e.object({constructPath:e.string().max(512),group:e.string().max(128),resourceType:e.string().max(256)}).strict(),j=e.object({version:e.literal(u),generatedAt:e.string(),appName:e.string(),services:e.array(c),lambdas:e.array(p),pattern:m.optional(),ecr:l.optional(),stacks:e.record(e.string(),d),resourceMap:e.record(e.string(),g).optional()}).strict();export{b as BUILDKIT_SECRET_ID_PATTERN,y as DockerBuildArgValueSchema,x as DockerBuildPartialSchema,i as DockerBuildSchema,f as DockerBuildSecretRefSchema,h as FJALL_MANIFEST_FILENAME,j as FjallManifestSchema,u as MANIFEST_SCHEMA_VERSION,l as ManifestEcrSchema,p as ManifestLambdaSchema,m as ManifestPatternSchema,c as ManifestServiceSchema,d as ManifestStackHashSchema,g as ResourceMapEntrySchema,M as mergeDockerBuild};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fjall/util",
3
- "version": "2.20.1",
3
+ "version": "2.22.0",
4
4
  "description": "Common utility methods",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -117,5 +117,5 @@
117
117
  "engines": {
118
118
  "node": ">=22.0.0"
119
119
  },
120
- "gitHead": "397880361ea533450ea5c888f7e7b9593254f663"
120
+ "gitHead": "448c771d258e87f0535ae7698f11438e0f7c57b1"
121
121
  }