@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 +1 -1
- package/dist/aws/deployProgress.d.ts +35 -0
- package/dist/aws/deployProgress.js +1 -0
- package/dist/aws/index.d.ts +2 -0
- package/dist/aws/index.js +1 -1
- package/dist/aws/maskFailureAnalysis.d.ts +18 -0
- package/dist/aws/maskFailureAnalysis.js +1 -0
- package/dist/deploy/approvalTokenOutput.d.ts +11 -0
- package/dist/deploy/approvalTokenOutput.js +1 -0
- package/dist/docker/DockerCli.d.ts +17 -0
- package/dist/docker/DockerCli.js +1 -1
- package/dist/docker/DockerCli.registry.d.ts +15 -3
- package/dist/docker/DockerCli.registry.js +3 -3
- package/dist/docker/bakeGuard.d.ts +91 -0
- package/dist/docker/bakeGuard.js +1 -0
- package/dist/docker/buildArgInference.d.ts +61 -0
- package/dist/docker/buildArgInference.js +1 -0
- package/dist/docker/buildxArgvBuilder.js +1 -1
- package/dist/docker/dockerCliSchemas.d.ts +8 -1
- package/dist/docker/dockerCliSchemas.js +1 -1
- package/dist/docker/ecrCredentialStore.d.ts +61 -0
- package/dist/docker/ecrCredentialStore.js +1 -0
- package/dist/docker/index.d.ts +3 -0
- package/dist/docker/index.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/manifest/index.d.ts +1 -1
- package/dist/manifest/index.js +1 -1
- package/dist/manifest/io.js +1 -1
- package/dist/manifest/schemas.d.ts +159 -9
- package/dist/manifest/schemas.js +1 -1
- package/package.json +2 -2
package/dist/.minified
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
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};
|
package/dist/aws/index.d.ts
CHANGED
|
@@ -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
|
|
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>>;
|
package/dist/docker/DockerCli.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{spawn as
|
|
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
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
2
|
-
`).toLowerCase().includes("unauthorized")?i(a("auth_failed",`docker push ${r} unauthorized`,{stderrTail:
|
|
3
|
-
`,!(t===void 0||p==="")&&t({id:r,status:
|
|
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
|
|
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.
|
|
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"]),
|
|
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};
|
package/dist/docker/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/docker/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{isSuccess as
|
|
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{
|
|
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};
|
package/dist/manifest/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/manifest/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{DockerBuildSchema as t,DockerBuildPartialSchema as
|
|
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};
|
package/dist/manifest/io.js
CHANGED
|
@@ -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
|
|
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
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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>>;
|
package/dist/manifest/schemas.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{z as
|
|
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.
|
|
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": "
|
|
120
|
+
"gitHead": "448c771d258e87f0535ae7698f11438e0f7c57b1"
|
|
121
121
|
}
|