@fjall/util 2.22.1 → 2.23.1

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
- 72 files minified at 2026-06-30T21:20:04.112Z
1
+ 73 files minified at 2026-07-02T12:17:29.289Z
@@ -114,6 +114,23 @@ export declare class DockerCli {
114
114
  * Best-effort: never throws.
115
115
  */
116
116
  logoutEcr(): Promise<void>;
117
+ /**
118
+ * Run `fn` inside a self-scoped ephemeral ECR session: `loginEcr(auth)` →
119
+ * `fn()` → `logoutEcr()` in a `finally`. The single home for the
120
+ * login→op→teardown ceremony that every self-scoped registry op
121
+ * (`buildAndPush`, `tagByDigest`, in BOTH the CLI and worker providers) would
122
+ * otherwise hand-copy. Flattens so callers get `Result<T, E>` rather than
123
+ * `Result<Result<T>>`: a login failure short-circuits through `mapLoginError`
124
+ * (which preserves each provider's masked failure prefix — the login is no
125
+ * longer inside the provider, so the provider cannot format the error itself);
126
+ * otherwise the caller's own Result flows through untouched. Teardown is
127
+ * best-effort — a `logoutEcr` throw cannot replace the operation result.
128
+ *
129
+ * Providers that bracket a WHOLE docker phase instead call `loginEcr` once at
130
+ * `beginEcrSession` and `logoutEcr` once at `endEcrSession`; this helper is
131
+ * the per-op fallback for standalone / unbracketed calls.
132
+ */
133
+ withEcrSession<T, E>(auth: EcrLoginArgs, fn: () => Promise<Result<T, E>>, mapLoginError: (error: DockerCliError) => E): Promise<Result<T, E>>;
117
134
  ensureBuilder(name: string): Promise<Result<{
118
135
  created: boolean;
119
136
  }, DockerCliError>>;
@@ -1 +1 @@
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
+ import{spawn as h}from"node:child_process";import{filterDangerousEnvVars as k,maskSensitiveOutput as E}from"../securityHelpers.js";import{abortChildProcess as b}from"./abortHelpers.js";import{DEFAULT_DOCKER_BIN as A,DOCKER_CLI_LOG_CATEGORY as x,STDERR_TAIL_LINE_MAX_CHARS as y,STDERR_TAIL_LINES as S}from"./dockerCliConstants.js";import{_buildxBuild as D,_imagetoolsInspect as C,_tagByDigest as I}from"./DockerCli.build.js";import{_assertBuildxAvailable as L,_detectDaemon as R,_ensureBuilder as O}from"./DockerCli.daemon.js";import{_imageInspect as P,_loginEcr as N,_pull as F,_push as G,_tag as K}from"./DockerCli.registry.js";import{failure as v,success as j}from"./result.js";class Q{state;constructor(e){const r=e.dockerBin!==void 0&&e.dockerBin!==""?e.dockerBin:A,n=k(e.env??process.env);this.state={logger:e.logger,dockerBin:r,env:n,abortSignal:e.abortSignal,ecrSession:void 0}}async buildxBuild(e,r){return D(this.state,e,r)}async tag(e,r){return K(this.state,e,r)}async tagByDigest(e,r,n){return I(this.state,e,r,n)}async push(e,r){return G(this.state,e,r)}async pull(e,r,n){return F(this.state,e,r,n)}async imageInspect(e){return P(this.state,e)}async imagetoolsInspect(e){return C(this.state,e)}async loginEcr(e){return N(this.state,e)}async logoutEcr(){const e=this.state.ecrSession;this.state.ecrSession=void 0,e!==void 0&&await e.dispose()}async withEcrSession(e,r,n){const o=await this.loginEcr(e);if(!o.success)return v(n(o.error));try{return await r()}finally{await this.logoutEcr().catch(()=>{})}}async ensureBuilder(e){return O(this.state,e)}async assertBuildxAvailable(){return L(this.state)}async detectDaemon(){return R(this.state)}}function H(t){const e=E(t);return e.length>y?e.slice(0,y)+"\u2026":e}function U(t,e,r){let n,o=r;if(r!==void 0&&Array.isArray(r.stderrTail)&&r.stderrTail.every(a=>typeof a=="string")){n=r.stderrTail.map(H);const{stderrTail:a,...s}=r;o=Object.keys(s).length>0?s:void 0}return{kind:t,message:e,...n!==void 0&&{stderrTail:n},...o!==void 0&&{details:o}}}function W(t){return U("daemon_unreachable",`Docker CLI is not available: ${t.spawnError??"unknown spawn failure"}`,{stderrTail:t.stderrTail})}function _(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 Z(t){return E(t)}function T(t){return t.ecrSession!==void 0?{...t.env,...t.ecrSession.env}:t.env}function ee(t,e){return new Promise(r=>{let n=!1,o=!1;const a=w(t.abortSignal,e.timeoutMs);let s;try{s=h(t.dockerBin,e.args,{shell:!1,env:T(t)})}catch(i){const d=i instanceof Error?i.message:String(i);r({exitCode:null,stdout:"",stderr:d,stderrTail:[d],aborted:!1,spawnError:d});return}let c="",l="";s.stdout?.on("data",i=>{c+=i.toString()}),s.stderr?.on("data",i=>{l+=i.toString()}),e.stdin!==void 0&&(s.stdin?.on("error",i=>{t.logger.debug(x,"stdin write error (typically EPIPE on early child exit)",{error:i.message})}),s.stdin?.write(e.stdin),s.stdin?.end());const g=()=>{n=!0,b(s)};a!==void 0&&(a.aborted?g():a.addEventListener("abort",g,{once:!0})),s.once("error",i=>{if(o)return;o=!0,a!==void 0&&a.removeEventListener("abort",g);const d=i instanceof Error?i.message:String(i);r({exitCode:null,stdout:c,stderr:d,stderrTail:[d],aborted:n,spawnError:d})}),s.once("close",i=>{o||(o=!0,a!==void 0&&a.removeEventListener("abort",g),r({exitCode:i,stdout:c,stderr:l,stderrTail:_(l,S),aborted:n}))})})}function re(t,e,r,n){return new Promise(o=>{let a=!1,s=!1;const c=w(t.abortSignal,e.timeoutMs);let l;try{l=h(t.dockerBin,e.args,{shell:!1,env:T(t)})}catch(u){const f=u instanceof Error?u.message:String(u);o({exitCode:null,stderrTail:[f],aborted:!1,spawnError:f,child:null});return}let g="",i="",d="";l.stdout?.on("data",u=>{g+=u.toString();const f=g.split(/\r?\n/);g=f.pop()??"";for(const p of f)r(p)}),l.stderr?.on("data",u=>{const f=u.toString();if(i+=f,n!==void 0){d+=f;const p=d.split(/\r?\n/);d=p.pop()??"";for(const B of p)n(B)}});const m=()=>{a=!0,b(l)};c!==void 0&&(c.aborted?m():c.addEventListener("abort",m,{once:!0})),l.once("error",u=>{if(s)return;s=!0,c!==void 0&&c.removeEventListener("abort",m);const f=u instanceof Error?u.message:String(u);o({exitCode:null,stderrTail:[f],aborted:a,spawnError:f,child:l})}),l.once("close",u=>{s||(s=!0,g!==""&&r(g),n!==void 0&&d!==""&&n(d),c!==void 0&&c.removeEventListener("abort",m),o({exitCode:u,stderrTail:_(i,S),aborted:a,child:l}))})})}function w(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{Q as DockerCli,v as failure,U as makeError,Z as maskOutput,ee as runDocker,W as spawnFailureToError,re as streamDocker,j as success,_ as tailLines};
@@ -1 +1 @@
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};
1
+ import{DEFAULT_BUILDER_NAME as i}from"./dockerCliConstants.js";function c(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){const o=u.source.kind==="file"?`src=${u.source.path}`:`env=${u.source.name}`;t.push("--secret",`id=${u.id},${o}`)}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{c as buildxArgvBuilder};
@@ -54,7 +54,6 @@ export declare const BuildxBuildArgsSchema: z.ZodObject<{
54
54
  name: z.ZodString;
55
55
  }, z.core.$strict>], "kind">;
56
56
  }, z.core.$strict>>>;
57
- dockerfileChecks: z.ZodOptional<z.ZodArray<z.ZodString>>;
58
57
  provenance: z.ZodBoolean;
59
58
  sbom: z.ZodBoolean;
60
59
  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"]),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};
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(),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};
@@ -21,6 +21,12 @@
21
21
  * - docker contexts (the active daemon endpoint — e.g. OrbStack, colima, a
22
22
  * remote TLS endpoint) are symlinked in, so a non-default `currentContext`
23
23
  * still resolves;
24
+ * - the `cli-plugins` directory is symlinked in, so `docker buildx` resolves
25
+ * under the redirected `DOCKER_CONFIG`. On macOS, Docker Desktop / OrbStack
26
+ * install buildx ONLY into `~/.docker/cli-plugins`; without this link every
27
+ * `docker buildx` call is "unknown command" for the whole session and the
28
+ * build dies at builder bootstrap (the Linux worker keeps buildx in a system
29
+ * plugin dir, so it is unaffected);
24
30
  * - the buildx builder store is pointed at the real one via `BUILDX_CONFIG`,
25
31
  * so the persistent "fjall" builder is reused rather than re-created;
26
32
  * - all non-credential config keys (proxies, HttpHeaders, currentContext,
@@ -1 +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};
1
+ import{mkdtemp as S,readFile as F,rm as g,stat as C,symlink as y,writeFile as _}from"node:fs/promises";import{homedir as j,tmpdir as H}from"node:os";import{join as o}from"node:path";import{maskSensitiveOutput as I}from"../securityHelpers.js";import{DOCKER_CLI_LOG_CATEGORY as s}from"./dockerCliConstants.js";function u(e){return I(e instanceof Error?e.message:String(e))}function N(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:j();return o(t,".docker")}function A(e,r){const t=e.BUILDX_CONFIG;return t!==void 0&&t!==""?t:o(r,"buildx")}async function B(e,r){let t;try{t=await F(o(e,"config.json"),"utf8")}catch{return{}}try{const i=JSON.parse(t);return i===null||typeof i!="object"||Array.isArray(i)?{}:i}catch(i){return r.warn(s,"Real docker config.json is not valid JSON; using a minimal ephemeral config",{error:u(i)}),{}}}async function L(e,r,t){const i=o(e,"contexts");try{if(!(await C(i)).isDirectory())return}catch{return}try{await y(i,o(r,"contexts"),"dir")}catch(a){t.warn(s,"Could not link docker contexts into the ephemeral config; a non-default docker context may not resolve for this build",{error:u(a)})}}async function J(e,r,t){const i=o(e,"cli-plugins");try{if(!(await C(i)).isDirectory())return}catch{return}try{await y(i,o(r,"cli-plugins"),"dir")}catch(a){t.warn(s,"Could not link docker cli-plugins into the ephemeral config; `docker buildx` may be unavailable for this build",{error:u(a)})}}async function $(e){const{registry:r,username:t,password:i,baseEnv:a,logger:c}=e,d=N(r),l=G(a),h=A(a,l),w=await B(l,c),{credsStore:k,credHelpers:O,auths:D,...x}=w,f={...O??{}};delete f[d];const E=Buffer.from(`${t}:${i}`,"utf8").toString("base64"),b={...D??{},[d]:{auth:E}},R={...x,auths:b,...Object.keys(f).length>0?{credHelpers:f}:{}};let p;try{const n=await S(o(H(),"fjall-ecr-auth-"));p=n,await _(o(n,"config.json"),JSON.stringify(R),{mode:384}),await L(l,n,c),await J(l,n,c),c.debug(s,"Established ephemeral ECR credential context",{registry:d,dockerConfig:n,buildxConfig:h});let m=!1;return{env:{DOCKER_CONFIG:n,BUILDX_CONFIG:h},async dispose(){m||(m=!0,await g(n,{recursive:!0,force:!0}).catch(v=>{c.warn(s,"Failed to remove ephemeral ECR credential dir",{path:n,error:u(v)})}))}}}catch(n){throw p!==void 0&&await g(p,{recursive:!0,force:!0}).catch(()=>{}),n}}export{$ as createEcrAuthSession};
package/dist/index.d.ts CHANGED
@@ -15,6 +15,7 @@ export { RESOURCE_CATEGORIES, type ResourceCategory, categoriseResource, getExpe
15
15
  export { parseGitRemoteUrl, type GitProvider, type ParsedGitRemote } from "./repo/gitRemoteParser.js";
16
16
  export { abbreviateRegion, AWS_REGIONS_METADATA, DEFAULT_REGION, getRegionInfo, MAX_SECONDARY_REGIONS, OPT_IN_REGION_CODES, optInRegionWarning, regions, suggestRegionForTimezone, type RegionCode, type RegionInfo } from "./infra/regions.js";
17
17
  export { SCOPE_VALUES, type TokenScope } from "./infra/tokenScopes.js";
18
+ export { SECRET_NAME_PATTERN, SECRET_NAME_ERROR, SSM_COMPONENT_PATTERN, SSM_COMPONENT_ERROR, SSM_STANDARD_MAX_VALUE_BYTES, SecretNamespaceSchema, type SecretNamespace, buildNamespaceParts, buildParameterPath, parseParameterPath, isManageablePath, parseDotEnv, escapeDotEnvValue } from "./secrets.js";
18
19
  export { ConnectionWireSchema, type ConnectionWire, ConnectionsListResponseSchema, type ConnectionsListResponse } from "./infra/connectionsWire.js";
19
20
  export { deriveRegionsFromOrgConfig, deriveTargets, deriveAllTargets, environmentOrTier, findTarget, generateTargetName, type OrgConfigRegions, type TargetAccount, type DerivedTarget } from "./targets.js";
20
21
  export { buildAppConfigPath } from "./repo/appPath.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{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};
1
+ import{DNS_APEX as o,getDomainExportNames as t}from"./infra/domainExports.js";import{BACKUP_VAULT_NAME as a}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 R,toValidDatabaseName as s,toScreamingSnake as N,capitalise as T,getSafeZoneName as C,accountConstructKey as p,hasAsciiStableConstructKey as O}from"./naming/caseConversion.js";import{findAccountNameCollision as c}from"./naming/accountNameCollision.js";import{defaultConnectedAccountName as P,suffixedAccountName as I,REGION_SHORT_CODES as u,findTrailingRegionShortCode as x,regionSuffixRejectionMessage as M}from"./naming/connectedAccountName.js";import{normaliseError as l,getErrorMessage as D,hasErrorCode as V,getErrorCode as U,getErrorStack as h,formatErrorString as G}from"./errorUtils.js";import{singleton as L}from"./async/singleton.js";import{DANGEROUS_ENV_VARS as H,filterDangerousEnvVars as F,maskSensitiveOutput as X,parseShellArgs as K}from"./securityHelpers.js";import{sleep as y}from"./async/sleep.js";import{mapSettledWithConcurrency as k}from"./async/concurrency.js";import{ACCOUNT_STAGES_WITH_ROOT as j,STRUCTURAL_ENVIRONMENTS as z,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 Ee}from"./environments.js";import{RESOURCE_CATEGORIES as ne,categoriseResource as ie,getExpectedDuration as Se,getFriendlyResourceType as _e}from"./resourceCategorisation.js";import{parseGitRemoteUrl as me}from"./repo/gitRemoteParser.js";import{abbreviateRegion as se,AWS_REGIONS_METADATA as Ne,DEFAULT_REGION as Te,getRegionInfo as Ce,MAX_SECONDARY_REGIONS as pe,OPT_IN_REGION_CODES as Oe,optInRegionWarning as fe,regions as ce,suggestRegionForTimezone as ge}from"./infra/regions.js";import{SCOPE_VALUES as Ie}from"./infra/tokenScopes.js";import{SECRET_NAME_PATTERN as xe,SECRET_NAME_ERROR as Me,SSM_COMPONENT_PATTERN as de,SSM_COMPONENT_ERROR as le,SSM_STANDARD_MAX_VALUE_BYTES as De,SecretNamespaceSchema as Ve,buildNamespaceParts as Ue,buildParameterPath as he,parseParameterPath as Ge,isManageablePath as ve,parseDotEnv as Le,escapeDotEnvValue as be}from"./secrets.js";import{ConnectionWireSchema as Fe,ConnectionsListResponseSchema as Xe}from"./infra/connectionsWire.js";import{deriveRegionsFromOrgConfig as We,deriveTargets as ye,deriveAllTargets as Be,environmentOrTier as ke,findTarget as Ye,generateTargetName as je}from"./targets.js";import{buildAppConfigPath as Ze}from"./repo/appPath.js";import{findInfrastructurePaths as we,findBoundaryPath as Je,isInfrastructureFile as Qe}from"./repo/findInfrastructurePaths.js";import{inferContainerFromCandidates as er}from"./repo/inferContainerFromCandidates.js";import{RESERVED_APP_NAMES as or,RESERVED_APP_NAME_MESSAGE as tr,isReservedAppName as Er}from"./naming/reservedAppNames.js";import{deriveContentHashTag as nr}from"./infra/deriveContentHashTag.js";import{MIGRATION_SNAPSHOT_NAME_PREFIX as Sr,EXPECTED_SCHEMA_VERSION_ENV as _r,EXPECTED_SCHEMA_VERSION_TOOL_ENV as Ar,EXPECTED_CH_SCHEMA_VERSION_ENV as mr,SCHEMA_ADMIN_USER_ENV as Rr,SCHEMA_ADMIN_PASSWORD_ENV as sr,PRISMA_MIGRATION_DIR_RE as Nr,CLICKHOUSE_MIGRATION_SKIP_RE as Tr}from"./migration/constants.js";export{Ee as ACCOUNT_ROLES,Z as ACCOUNT_STAGES,j as ACCOUNT_STAGES_WITH_ROOT,q as ACCOUNT_STAGE_LABELS,J as ACCOUNT_TIERS,i as APPROVAL_TOKEN_OUTPUT_PREFIX,Ne as AWS_REGIONS_METADATA,Q as AccountTierSchema,a as BACKUP_VAULT_NAME,Tr as CLICKHOUSE_MIGRATION_SKIP_RE,Fe as ConnectionWireSchema,Xe as ConnectionsListResponseSchema,H as DANGEROUS_ENV_VARS,Te as DEFAULT_REGION,o as DNS_APEX,mr as EXPECTED_CH_SCHEMA_VERSION_ENV,_r as EXPECTED_SCHEMA_VERSION_ENV,Ar as EXPECTED_SCHEMA_VERSION_TOOL_ENV,pe as MAX_SECONDARY_REGIONS,Sr as MIGRATION_SNAPSHOT_NAME_PREFIX,Oe as OPT_IN_REGION_CODES,Nr as PRISMA_MIGRATION_DIR_RE,u as REGION_SHORT_CODES,or as RESERVED_APP_NAMES,tr as RESERVED_APP_NAME_MESSAGE,ne as RESOURCE_CATEGORIES,sr as SCHEMA_ADMIN_PASSWORD_ENV,Rr as SCHEMA_ADMIN_USER_ENV,Ie as SCOPE_VALUES,Me as SECRET_NAME_ERROR,xe as SECRET_NAME_PATTERN,le as SSM_COMPONENT_ERROR,de as SSM_COMPONENT_PATTERN,De as SSM_STANDARD_MAX_VALUE_BYTES,z as STRUCTURAL_ENVIRONMENTS,Ve as SecretNamespaceSchema,se as abbreviateRegion,p as accountConstructKey,oe as accountTier,Ze as buildAppConfigPath,Ue as buildNamespaceParts,he as buildParameterPath,T as capitalise,ie as categoriseResource,P as defaultConnectedAccountName,Be as deriveAllTargets,nr as deriveContentHashTag,We as deriveRegionsFromOrgConfig,ye as deriveTargets,ke as environmentOrTier,ee as environmentToTier,be as escapeDotEnvValue,F as filterDangerousEnvVars,c as findAccountNameCollision,Je as findBoundaryPath,we as findInfrastructurePaths,Ye as findTarget,x as findTrailingRegionShortCode,G as formatErrorString,je as generateTargetName,t as getDomainExportNames,te as getEnvironmentLabel,U as getErrorCode,D as getErrorMessage,h as getErrorStack,Se as getExpectedDuration,_e as getFriendlyResourceType,Ce as getRegionInfo,C as getSafeZoneName,O as hasAsciiStableConstructKey,V as hasErrorCode,_ as imageTagParameterName,er as inferContainerFromCandidates,w as isAccountStage,$ as isAccountTier,Qe as isInfrastructureFile,ve as isManageablePath,Er as isReservedAppName,k as mapSettledWithConcurrency,X as maskSensitiveOutput,l as normaliseError,fe as optInRegionWarning,Le as parseDotEnv,me as parseGitRemoteUrl,Ge as parseParameterPath,K as parseShellArgs,M as regionSuffixRejectionMessage,ce as regions,L as singleton,y as sleep,re as stageFromWireEnvironment,I as suffixedAccountName,ge as suggestRegionForTimezone,R as toKebab,m as toPascalCase,N as toScreamingSnake,s as toValidDatabaseName};
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Canonical Fjall secrets namespace module — the single source of truth for
3
+ * the SSM Parameter Store hierarchy shared by the CLI, the CDK constructs,
4
+ * the generator, and the webapp:
5
+ *
6
+ * /<app>/<NAME> app-root secrets
7
+ * /<app>/<cluster>/<NAME> cluster secrets
8
+ * /<app>/<cluster>/<service>/<NAME> service secrets
9
+ * /<app>/lambda/<function>/<NAME> lambda secrets
10
+ *
11
+ * Browser-safe by contract: no Node built-ins (no `Buffer`, no `node:fs`) —
12
+ * this module is exported from the `@fjall/util` barrel, which client bundles
13
+ * import.
14
+ */
15
+ import { z } from "zod";
16
+ export declare const SECRET_NAME_PATTERN: RegExp;
17
+ export declare const SECRET_NAME_ERROR = "Secret name must start with a letter or underscore and contain only letters, numbers, underscores, hyphens, or periods";
18
+ export declare const SSM_COMPONENT_PATTERN: RegExp;
19
+ export declare const SSM_COMPONENT_ERROR = "Must start with a letter and contain only letters, numbers, periods, hyphens, or underscores";
20
+ /** SSM Standard-tier parameter values are capped at 4 KB of UTF-8 bytes, not characters. */
21
+ export declare const SSM_STANDARD_MAX_VALUE_BYTES = 4096;
22
+ /**
23
+ * Namespace for a secret's location in the hierarchy. Service requires
24
+ * cluster; lambda is mutually exclusive with cluster/service; the cluster
25
+ * name "lambda" is reserved because it is the path marker for lambda scope.
26
+ */
27
+ export declare const SecretNamespaceSchema: z.ZodObject<{
28
+ app: z.ZodString;
29
+ cluster: z.ZodOptional<z.ZodString>;
30
+ service: z.ZodOptional<z.ZodString>;
31
+ lambda: z.ZodOptional<z.ZodString>;
32
+ }, z.core.$strict>;
33
+ export type SecretNamespace = z.infer<typeof SecretNamespaceSchema>;
34
+ /**
35
+ * Build the ordered path segments for a namespace. A service without a
36
+ * cluster is dropped (that shape is not encodable in the hierarchy — the
37
+ * schema rejects it).
38
+ */
39
+ export declare function buildNamespaceParts(ns: SecretNamespace): string[];
40
+ /**
41
+ * Build the full SSM parameter path from a namespace and optional secret
42
+ * name. Without a name, returns the namespace root path.
43
+ */
44
+ export declare function buildParameterPath(namespace: SecretNamespace, name?: string): string;
45
+ /**
46
+ * Parse a full SSM parameter path into namespace and secret name — the
47
+ * inverse of buildParameterPath. Returns null for paths with fewer than two
48
+ * segments.
49
+ *
50
+ * Lambda decode requires four segments: the 3-segment `/app/lambda/NAME`
51
+ * shape deliberately decodes as cluster "lambda" (not a lambda scope), so
52
+ * the reserved-cluster refine marks it unmanageable instead of NAME being
53
+ * mistaken for a function name.
54
+ */
55
+ export declare function parseParameterPath(fullPath: string): {
56
+ namespace: SecretNamespace;
57
+ name: string;
58
+ } | null;
59
+ /**
60
+ * Whether a parameter path is manageable through Fjall's namespace
61
+ * hierarchy: it decodes to a schema-valid namespace AND rebuilding the path
62
+ * from the decoded parts reproduces it exactly. False for paths outside the
63
+ * convention (five-plus segments, the reserved 3-segment `/app/lambda/NAME`
64
+ * shape) — a mutation rebuilt from such a path's namespace would target a
65
+ * DIFFERENT parameter.
66
+ */
67
+ export declare function isManageablePath(fullPath: string): boolean;
68
+ /**
69
+ * Parse .env file contents into key-value pairs. Handles single- and
70
+ * double-quoted values, the `export KEY=value` dialect, comments, and empty
71
+ * lines; keys failing SECRET_NAME_PATTERN are skipped.
72
+ *
73
+ * Double-quoted values unescape exactly the sequence set escapeDotEnvValue
74
+ * emits, so an exported file re-imports to the original values. Single-quoted
75
+ * and bare values stay literal.
76
+ *
77
+ * **Limitation**: raw multiline values are not supported — an unquoted value
78
+ * containing literal newlines (private keys, certificates) is truncated at
79
+ * the first newline. Exported files are unaffected: escapeDotEnvValue encodes
80
+ * newlines as `\n`, which double-quoted parsing restores.
81
+ */
82
+ export declare function parseDotEnv(content: string): Record<string, string>;
83
+ /**
84
+ * Escape a value for double-quoted dotenv output. Backslash is replaced
85
+ * first — later replacements introduce backslashes that must not be
86
+ * re-escaped. parseDotEnv reverses exactly this sequence set for
87
+ * double-quoted values, making export → import a lossless round trip.
88
+ */
89
+ export declare function escapeDotEnvValue(value: string): string;
@@ -0,0 +1,3 @@
1
+ import{z as p}from"zod";const u=/^[a-zA-Z_][a-zA-Z0-9._-]*$/,_="Secret name must start with a letter or underscore and contain only letters, numbers, underscores, hyphens, or periods",m=/^[a-zA-Z][a-zA-Z0-9._-]*$/,d="Must start with a letter and contain only letters, numbers, periods, hyphens, or underscores",S=4096,c=p.string().regex(m,d),f=p.object({app:c.describe("Application name"),cluster:c.optional().describe("Cluster name"),service:c.optional().describe("Service name"),lambda:c.optional().describe("Lambda function name")}).strict().refine(t=>!(t.service&&!t.cluster),{message:"Cluster is required when service is specified"}).refine(t=>!(t.lambda&&(t.cluster||t.service)),{message:"Lambda is mutually exclusive with cluster and service"}).refine(t=>t.cluster!=="lambda",{message:'The cluster name "lambda" is reserved for lambda-scoped parameter paths'});function h(t){const e=[t.app];return t.lambda?e.push("lambda",t.lambda):(t.cluster&&e.push(t.cluster),t.service&&t.cluster&&e.push(t.service)),e}function b(t,e){const n=h(t);return e&&n.push(e),"/"+n.join("/")}function g(t){const e=t.split("/").filter(Boolean);if(e.length<2)return null;const n=e[e.length-1]??"",s={app:e[0]??""};return e.length>=4&&e[1]==="lambda"?s.lambda=e[2]:e.length>=3&&(s.cluster=e[1],e.length>=4&&(s.service=e[2])),{namespace:s,name:n}}function v(t){const e=g(t);return e?f.safeParse(e.namespace).success&&b(e.namespace,e.name)===t:!1}function A(t){const e={};for(const n of t.split(`
2
+ `)){const s=n.trim();if(!s||s.startsWith("#"))continue;const i=s.replace(/^export\s+/,""),o=i.indexOf("=");if(o===-1)continue;const a=i.slice(0,o).trim();if(!u.test(a))continue;const r=i.slice(o+1);r.length>=2&&r.startsWith('"')&&r.endsWith('"')?e[a]=r.slice(1,-1).replace(/\\([\\"$`nr])/g,(x,l)=>l==="n"?`
3
+ `:l==="r"?"\r":l):r.length>=2&&r.startsWith("'")&&r.endsWith("'")?e[a]=r.slice(1,-1):e[a]=r}return e}function N(t){return t.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/\$/g,"\\$").replace(/`/g,"\\`").replace(/\n/g,"\\n").replace(/\r/g,"\\r")}export{_ as SECRET_NAME_ERROR,u as SECRET_NAME_PATTERN,d as SSM_COMPONENT_ERROR,m as SSM_COMPONENT_PATTERN,S as SSM_STANDARD_MAX_VALUE_BYTES,f as SecretNamespaceSchema,h as buildNamespaceParts,b as buildParameterPath,N as escapeDotEnvValue,v as isManageablePath,A as parseDotEnv,g as parseParameterPath};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fjall/util",
3
- "version": "2.22.1",
3
+ "version": "2.23.1",
4
4
  "description": "Common utility methods",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -77,6 +77,10 @@
77
77
  "./securityHelpers": {
78
78
  "types": "./dist/securityHelpers.d.ts",
79
79
  "default": "./dist/securityHelpers.js"
80
+ },
81
+ "./secrets": {
82
+ "types": "./dist/secrets.d.ts",
83
+ "default": "./dist/secrets.js"
80
84
  }
81
85
  },
82
86
  "files": [
@@ -117,5 +121,5 @@
117
121
  "engines": {
118
122
  "node": ">=22.0.0"
119
123
  },
120
- "gitHead": "ec04f1739f19e4b5212899df7f733ee0d1f06ec3"
124
+ "gitHead": "749133c6c5cfe24a09a401869f2193c7d177a324"
121
125
  }