@fjall/util 2.22.0 → 2.23.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/docker/DockerCli.d.ts +17 -0
- package/dist/docker/DockerCli.js +1 -1
- package/dist/docker/buildxArgvBuilder.js +1 -1
- package/dist/docker/dockerCliSchemas.d.ts +0 -1
- package/dist/docker/dockerCliSchemas.js +1 -1
- package/dist/docker/ecrCredentialStore.d.ts +6 -0
- package/dist/docker/ecrCredentialStore.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/secrets.d.ts +89 -0
- package/dist/secrets.js +3 -0
- package/package.json +6 -2
package/dist/.minified
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
73 files minified at 2026-07-02T09:02:28.381Z
|
|
@@ -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>>;
|
package/dist/docker/DockerCli.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{spawn as
|
|
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
|
|
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(),
|
|
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
|
|
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
|
|
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;
|
package/dist/secrets.js
ADDED
|
@@ -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.
|
|
3
|
+
"version": "2.23.0",
|
|
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": "
|
|
124
|
+
"gitHead": "dadd84e6118f03252877974eec755e812cb5b42f"
|
|
121
125
|
}
|