@ttt-productions/repo-guards 0.1.12 → 0.1.13
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/modules/ui/fonts.d.ts +3 -0
- package/dist/modules/ui/fonts.d.ts.map +1 -0
- package/dist/modules/ui/fonts.js +52 -0
- package/dist/modules/ui/fonts.js.map +1 -0
- package/dist/modules/ui/guards.d.ts +1 -0
- package/dist/modules/ui/guards.d.ts.map +1 -1
- package/dist/modules/ui/guards.js +2 -0
- package/dist/modules/ui/guards.js.map +1 -1
- package/dist/node/tooling/apphosting-build.d.ts +7 -0
- package/dist/node/tooling/apphosting-build.d.ts.map +1 -0
- package/dist/node/tooling/apphosting-build.js +34 -0
- package/dist/node/tooling/apphosting-build.js.map +1 -0
- package/dist/node/tooling/apphosting-env.d.ts +36 -0
- package/dist/node/tooling/apphosting-env.d.ts.map +1 -0
- package/dist/node/tooling/apphosting-env.js +114 -0
- package/dist/node/tooling/apphosting-env.js.map +1 -0
- package/dist/node/tooling/callable-invokers.d.ts +22 -0
- package/dist/node/tooling/callable-invokers.d.ts.map +1 -0
- package/dist/node/tooling/callable-invokers.js +37 -0
- package/dist/node/tooling/callable-invokers.js.map +1 -0
- package/dist/node/tooling/cleanup.d.ts +5 -0
- package/dist/node/tooling/cleanup.d.ts.map +1 -0
- package/dist/node/tooling/cleanup.js +22 -0
- package/dist/node/tooling/cleanup.js.map +1 -0
- package/dist/node/tooling/cloud-checks.d.ts +29 -0
- package/dist/node/tooling/cloud-checks.d.ts.map +1 -0
- package/dist/node/tooling/cloud-checks.js +162 -0
- package/dist/node/tooling/cloud-checks.js.map +1 -0
- package/dist/node/tooling/deployed-functions.d.ts +37 -0
- package/dist/node/tooling/deployed-functions.d.ts.map +1 -0
- package/dist/node/tooling/deployed-functions.js +67 -0
- package/dist/node/tooling/deployed-functions.js.map +1 -0
- package/dist/node/tooling/dev-login.d.ts +34 -0
- package/dist/node/tooling/dev-login.d.ts.map +1 -0
- package/dist/node/tooling/dev-login.js +118 -0
- package/dist/node/tooling/dev-login.js.map +1 -0
- package/dist/node/tooling/dev-stack.d.ts +23 -0
- package/dist/node/tooling/dev-stack.d.ts.map +1 -0
- package/dist/node/tooling/dev-stack.js +103 -0
- package/dist/node/tooling/dev-stack.js.map +1 -0
- package/dist/node/tooling/emulator-seed.d.ts +7 -0
- package/dist/node/tooling/emulator-seed.d.ts.map +1 -0
- package/dist/node/tooling/emulator-seed.js +18 -0
- package/dist/node/tooling/emulator-seed.js.map +1 -0
- package/dist/node/tooling/field-lineage.d.ts +24 -0
- package/dist/node/tooling/field-lineage.d.ts.map +1 -0
- package/dist/node/tooling/field-lineage.js +70 -0
- package/dist/node/tooling/field-lineage.js.map +1 -0
- package/dist/node/tooling/firebase-projects-file.d.ts +4 -0
- package/dist/node/tooling/firebase-projects-file.d.ts.map +1 -0
- package/dist/node/tooling/firebase-projects-file.js +8 -0
- package/dist/node/tooling/firebase-projects-file.js.map +1 -0
- package/dist/node/tooling/firebase-projects.d.ts +14 -0
- package/dist/node/tooling/firebase-projects.d.ts.map +1 -0
- package/dist/node/tooling/firebase-projects.js +29 -0
- package/dist/node/tooling/firebase-projects.js.map +1 -0
- package/dist/node/tooling/index.d.ts +23 -0
- package/dist/node/tooling/index.d.ts.map +1 -0
- package/dist/node/tooling/index.js +25 -0
- package/dist/node/tooling/index.js.map +1 -0
- package/dist/node/tooling/playbook-refs.d.ts +22 -0
- package/dist/node/tooling/playbook-refs.d.ts.map +1 -0
- package/dist/node/tooling/playbook-refs.js +79 -0
- package/dist/node/tooling/playbook-refs.js.map +1 -0
- package/dist/node/tooling/playwright-report.d.ts +46 -0
- package/dist/node/tooling/playwright-report.d.ts.map +1 -0
- package/dist/node/tooling/playwright-report.js +67 -0
- package/dist/node/tooling/playwright-report.js.map +1 -0
- package/dist/node/tooling/quiet-gate.d.ts +63 -0
- package/dist/node/tooling/quiet-gate.d.ts.map +1 -0
- package/dist/node/tooling/quiet-gate.js +380 -0
- package/dist/node/tooling/quiet-gate.js.map +1 -0
- package/dist/node/tooling/release-scope.d.ts +105 -0
- package/dist/node/tooling/release-scope.d.ts.map +1 -0
- package/dist/node/tooling/release-scope.js +254 -0
- package/dist/node/tooling/release-scope.js.map +1 -0
- package/dist/node/tooling/release-verdict.d.ts +43 -0
- package/dist/node/tooling/release-verdict.d.ts.map +1 -0
- package/dist/node/tooling/release-verdict.js +123 -0
- package/dist/node/tooling/release-verdict.js.map +1 -0
- package/dist/node/tooling/release.d.ts +26 -0
- package/dist/node/tooling/release.d.ts.map +1 -0
- package/dist/node/tooling/release.js +129 -0
- package/dist/node/tooling/release.js.map +1 -0
- package/dist/node/tooling/shell.d.ts +18 -0
- package/dist/node/tooling/shell.d.ts.map +1 -0
- package/dist/node/tooling/shell.js +49 -0
- package/dist/node/tooling/shell.js.map +1 -0
- package/dist/node/tooling/storage-cors.d.ts +17 -0
- package/dist/node/tooling/storage-cors.d.ts.map +1 -0
- package/dist/node/tooling/storage-cors.js +41 -0
- package/dist/node/tooling/storage-cors.js.map +1 -0
- package/dist/node/tooling/vitest-report.d.ts +74 -0
- package/dist/node/tooling/vitest-report.d.ts.map +1 -0
- package/dist/node/tooling/vitest-report.js +92 -0
- package/dist/node/tooling/vitest-report.js.map +1 -0
- package/dist/node/tooling/vitest-unhandled-errors.d.ts +12 -0
- package/dist/node/tooling/vitest-unhandled-errors.d.ts.map +1 -0
- package/dist/node/tooling/vitest-unhandled-errors.js +52 -0
- package/dist/node/tooling/vitest-unhandled-errors.js.map +1 -0
- package/package.json +5 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fonts.d.ts","sourceRoot":"","sources":["../../../src/modules/ui/fonts.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAezE,eAAO,MAAM,oBAAoB,EAAE,sBAAsB,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAoC9E,CAAC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// The app's fonts are self-hosted: `next/font/google` downloads each face when the app is BUILT, so
|
|
2
|
+
// every build — local, CI, and every deploy — depends on Google's font service answering in the
|
|
3
|
+
// one shape Next's font loader reads, and an answer in any other shape fails the whole build. The
|
|
4
|
+
// faces live in the repository and load through `next/font/local`.
|
|
5
|
+
import { reviewedAllowlist } from '../../shapes/shapes.js';
|
|
6
|
+
import { objectProblem } from '../../support/facts.js';
|
|
7
|
+
import { APP_CODE } from './styling.js';
|
|
8
|
+
const GOOGLE_FONTS = 'next/font/google';
|
|
9
|
+
function googleFontImports(file) {
|
|
10
|
+
if (file.kind !== 'code')
|
|
11
|
+
return [];
|
|
12
|
+
return file.imports
|
|
13
|
+
.filter(({ spec }) => spec === GOOGLE_FONTS || spec.startsWith(`${GOOGLE_FONTS}/`))
|
|
14
|
+
.map((site) => ({ index: site.specifier.getStart(file.sourceFile), detail: site.spec }));
|
|
15
|
+
}
|
|
16
|
+
export const selfHostedFontsGuard = {
|
|
17
|
+
id: 'selfHostedFonts',
|
|
18
|
+
absentWhen: false,
|
|
19
|
+
factProblems(facts) {
|
|
20
|
+
const problems = [];
|
|
21
|
+
objectProblem(facts, 'facts', problems);
|
|
22
|
+
return problems;
|
|
23
|
+
},
|
|
24
|
+
build() {
|
|
25
|
+
return [
|
|
26
|
+
reviewedAllowlist({
|
|
27
|
+
title: 'the app loads its fonts from the repository, never from Google at build time',
|
|
28
|
+
description: 'next/font/google downloads each face when the app is built, so every build depends on the font service ' +
|
|
29
|
+
"answering in the one shape Next's loader reads, and any other answer fails the build. Keep each face's " +
|
|
30
|
+
'.woff2 and its license in the repository and load it through next/font/local.',
|
|
31
|
+
scope: APP_CODE,
|
|
32
|
+
match: googleFontImports,
|
|
33
|
+
allowlist: [],
|
|
34
|
+
samples: {
|
|
35
|
+
flags: [
|
|
36
|
+
{ path: 'src/app/layout.tsx', text: "import { Inter } from 'next/font/google';" },
|
|
37
|
+
{ path: 'src/app/layout.tsx', text: "const fonts = await import('next/font/google');" },
|
|
38
|
+
{ path: 'src/app/layout.tsx', text: "export { Inter } from 'next/font/google';" },
|
|
39
|
+
],
|
|
40
|
+
passes: [
|
|
41
|
+
{ path: 'src/app/layout.tsx', text: "import localFont from 'next/font/local';" },
|
|
42
|
+
{
|
|
43
|
+
path: 'src/app/layout.tsx',
|
|
44
|
+
text: "const note = 'next/font/google is not used here';",
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
}),
|
|
49
|
+
];
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
//# sourceMappingURL=fonts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fonts.js","sourceRoot":"","sources":["../../../src/modules/ui/fonts.ts"],"names":[],"mappings":"AAAA,oGAAoG;AACpG,gGAAgG;AAChG,kGAAkG;AAClG,mEAAmE;AAGnE,OAAO,EAAE,iBAAiB,EAAY,MAAM,wBAAwB,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAEvD,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAExC,MAAM,YAAY,GAAG,kBAAkB,CAAC;AAExC,SAAS,iBAAiB,CAAC,IAAc;IACvC,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM;QAAE,OAAO,EAAE,CAAC;IACpC,OAAO,IAAI,CAAC,OAAO;SAChB,MAAM,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,IAAI,KAAK,YAAY,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,YAAY,GAAG,CAAC,CAAC;SAClF,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;AAC7F,CAAC;AAED,MAAM,CAAC,MAAM,oBAAoB,GAAkD;IACjF,EAAE,EAAE,iBAAiB;IACrB,UAAU,EAAE,KAAK;IACjB,YAAY,CAAC,KAAK;QAChB,MAAM,QAAQ,GAAa,EAAE,CAAC;QAC9B,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC;QACxC,OAAO,QAAQ,CAAC;IAClB,CAAC;IACD,KAAK;QACH,OAAO;YACL,iBAAiB,CAAC;gBAChB,KAAK,EAAE,8EAA8E;gBACrF,WAAW,EACT,yGAAyG;oBACzG,yGAAyG;oBACzG,+EAA+E;gBACjF,KAAK,EAAE,QAAQ;gBACf,KAAK,EAAE,iBAAiB;gBACxB,SAAS,EAAE,EAAE;gBACb,OAAO,EAAE;oBACP,KAAK,EAAE;wBACL,EAAE,IAAI,EAAE,oBAAoB,EAAE,IAAI,EAAE,2CAA2C,EAAE;wBACjF,EAAE,IAAI,EAAE,oBAAoB,EAAE,IAAI,EAAE,iDAAiD,EAAE;wBACvF,EAAE,IAAI,EAAE,oBAAoB,EAAE,IAAI,EAAE,2CAA2C,EAAE;qBAClF;oBACD,MAAM,EAAE;wBACN,EAAE,IAAI,EAAE,oBAAoB,EAAE,IAAI,EAAE,0CAA0C,EAAE;wBAChF;4BACE,IAAI,EAAE,oBAAoB;4BAC1B,IAAI,EAAE,mDAAmD;yBAC1D;qBACF;iBACF;aACF,CAAC;SACH,CAAC;IACJ,CAAC;CACF,CAAC","sourcesContent":["// The app's fonts are self-hosted: `next/font/google` downloads each face when the app is BUILT, so\n// every build — local, CI, and every deploy — depends on Google's font service answering in the\n// one shape Next's font loader reads, and an answer in any other shape fails the whole build. The\n// faces live in the repository and load through `next/font/local`.\n\nimport type { GenericGuardDefinition } from '../../node/suite/config.js';\nimport { reviewedAllowlist, type Hit } from '../../shapes/shapes.js';\nimport { objectProblem } from '../../support/facts.js';\nimport type { FileView } from '../../reader/view.js';\nimport { APP_CODE } from './styling.js';\n\nconst GOOGLE_FONTS = 'next/font/google';\n\nfunction googleFontImports(file: FileView): Hit[] {\n if (file.kind !== 'code') return [];\n return file.imports\n .filter(({ spec }) => spec === GOOGLE_FONTS || spec.startsWith(`${GOOGLE_FONTS}/`))\n .map((site) => ({ index: site.specifier.getStart(file.sourceFile), detail: site.spec }));\n}\n\nexport const selfHostedFontsGuard: GenericGuardDefinition<Record<string, never>> = {\n id: 'selfHostedFonts',\n absentWhen: false,\n factProblems(facts) {\n const problems: string[] = [];\n objectProblem(facts, 'facts', problems);\n return problems;\n },\n build() {\n return [\n reviewedAllowlist({\n title: 'the app loads its fonts from the repository, never from Google at build time',\n description:\n 'next/font/google downloads each face when the app is built, so every build depends on the font service ' +\n \"answering in the one shape Next's loader reads, and any other answer fails the build. Keep each face's \" +\n '.woff2 and its license in the repository and load it through next/font/local.',\n scope: APP_CODE,\n match: googleFontImports,\n allowlist: [],\n samples: {\n flags: [\n { path: 'src/app/layout.tsx', text: \"import { Inter } from 'next/font/google';\" },\n { path: 'src/app/layout.tsx', text: \"const fonts = await import('next/font/google');\" },\n { path: 'src/app/layout.tsx', text: \"export { Inter } from 'next/font/google';\" },\n ],\n passes: [\n { path: 'src/app/layout.tsx', text: \"import localFont from 'next/font/local';\" },\n {\n path: 'src/app/layout.tsx',\n text: \"const note = 'next/font/google is not used here';\",\n },\n ],\n },\n }),\n ];\n },\n};\n"]}
|
|
@@ -28,6 +28,7 @@ export interface UiGuardFacts {
|
|
|
28
28
|
pendingNameSeat: PendingNameSeatFacts;
|
|
29
29
|
accessibleControls: AccessibleControlFacts;
|
|
30
30
|
wholeInputNumbers: WholeInputNumberFacts;
|
|
31
|
+
selfHostedFonts: Record<string, never>;
|
|
31
32
|
}
|
|
32
33
|
export declare const UI_GUARDS: ReadonlyArray<GenericGuardDefinition<never>>;
|
|
33
34
|
//# sourceMappingURL=guards.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"guards.d.ts","sourceRoot":"","sources":["../../../src/modules/ui/guards.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AACzE,OAAO,EAGL,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC3B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAKL,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACrB,MAAM,cAAc,CAAC;AACtB,OAAO,EAIL,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACpB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAML,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EACzB,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EACnB,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"guards.d.ts","sourceRoot":"","sources":["../../../src/modules/ui/guards.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AACzE,OAAO,EAGL,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC3B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAKL,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACrB,MAAM,cAAc,CAAC;AACtB,OAAO,EAIL,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACpB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAML,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EACzB,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EACnB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAe,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EAOL,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EAC1B,MAAM,cAAc,CAAC;AAEtB,0DAA0D;AAC1D,MAAM,WAAW,YAAY;IAC3B,aAAa,EAAE,iBAAiB,CAAC;IACjC,iBAAiB,EAAE,oBAAoB,CAAC;IACxC,eAAe,EAAE,mBAAmB,CAAC;IACrC,eAAe,EAAE,oBAAoB,CAAC;IACtC,WAAW,EAAE,eAAe,CAAC;IAC7B,YAAY,EAAE,WAAW,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IACvC,sBAAsB,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAC9C,qBAAqB,EAAE,oBAAoB,CAAC;IAC5C,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IACnC,4BAA4B,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IACpD,gBAAgB,EAAE,eAAe,CAAC;IAClC,eAAe,EAAE,oBAAoB,CAAC;IACtC,eAAe,EAAE,gBAAgB,CAAC;IAClC,cAAc,EAAE,aAAa,CAAC;IAC9B,UAAU,EAAE,cAAc,CAAC;IAC3B,cAAc,EAAE,cAAc,CAAC;IAC/B,YAAY,EAAE,gBAAgB,CAAC;IAC/B,eAAe,EAAE,oBAAoB,CAAC;IACtC,kBAAkB,EAAE,sBAAsB,CAAC;IAC3C,iBAAiB,EAAE,qBAAqB,CAAC;IACzC,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;CACxC;AAED,eAAO,MAAM,SAAS,EAAE,aAAa,CAAC,sBAAsB,CAAC,KAAK,CAAC,CAuBjB,CAAC"}
|
|
@@ -4,6 +4,7 @@ import { accessibleControlsGuard, wholeInputNumbersGuard, } from './accessibilit
|
|
|
4
4
|
import { stylesheetEntryGuard, tailwindSourcesGuard, themeTokensGuard, utilityCollisionGuard, } from './cascade.js';
|
|
5
5
|
import { enumLabelsGuard, schemaBoundsGuard, textEntryGuard, } from './definitions.js';
|
|
6
6
|
import { layoutOrderGuard, loadingFeedbackGuard, navigationGuard, pendingNameSeatGuard, showMoreGuard, } from './interaction.js';
|
|
7
|
+
import { selfHostedFontsGuard } from './fonts.js';
|
|
7
8
|
import { motionGuard } from './motion.js';
|
|
8
9
|
import { colorLiteralsGuard, scaleClassesGuard, shadowOwnershipGuard, stackRowsGuard, statusForegroundGuard, unavailableAffordanceGuard, } from './styling.js';
|
|
9
10
|
export const UI_GUARDS = Object.freeze([
|
|
@@ -28,5 +29,6 @@ export const UI_GUARDS = Object.freeze([
|
|
|
28
29
|
pendingNameSeatGuard,
|
|
29
30
|
accessibleControlsGuard,
|
|
30
31
|
wholeInputNumbersGuard,
|
|
32
|
+
selfHostedFontsGuard,
|
|
31
33
|
]);
|
|
32
34
|
//# sourceMappingURL=guards.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"guards.js","sourceRoot":"","sources":["../../../src/modules/ui/guards.ts"],"names":[],"mappings":"AAAA,oKAAoK;AACpK,iCAAiC;AAGjC,OAAO,EACL,uBAAuB,EACvB,sBAAsB,GAGvB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,gBAAgB,EAChB,qBAAqB,GAItB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,cAAc,GAIf,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,eAAe,EACf,oBAAoB,EACpB,aAAa,GAMd,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,WAAW,EAAoB,MAAM,aAAa,CAAC;AAC5D,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,oBAAoB,EACpB,cAAc,EACd,qBAAqB,EACrB,0BAA0B,GAG3B,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"guards.js","sourceRoot":"","sources":["../../../src/modules/ui/guards.ts"],"names":[],"mappings":"AAAA,oKAAoK;AACpK,iCAAiC;AAGjC,OAAO,EACL,uBAAuB,EACvB,sBAAsB,GAGvB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,gBAAgB,EAChB,qBAAqB,GAItB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,cAAc,GAIf,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,eAAe,EACf,oBAAoB,EACpB,aAAa,GAMd,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAClD,OAAO,EAAE,WAAW,EAAoB,MAAM,aAAa,CAAC;AAC5D,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,oBAAoB,EACpB,cAAc,EACd,qBAAqB,EACrB,0BAA0B,GAG3B,MAAM,cAAc,CAAC;AA4BtB,MAAM,CAAC,MAAM,SAAS,GAAiD,MAAM,CAAC,MAAM,CAAC;IACnF,kBAAkB;IAClB,iBAAiB;IACjB,oBAAoB;IACpB,oBAAoB;IACpB,gBAAgB;IAChB,WAAW;IACX,oBAAoB;IACpB,qBAAqB;IACrB,qBAAqB;IACrB,cAAc;IACd,0BAA0B;IAC1B,eAAe;IACf,oBAAoB;IACpB,gBAAgB;IAChB,aAAa;IACb,eAAe;IACf,cAAc;IACd,iBAAiB;IACjB,oBAAoB;IACpB,uBAAuB;IACvB,sBAAsB;IACtB,oBAAoB;CAC2B,CAAC,CAAC","sourcesContent":["// The generic guards over the UI system: styling, theme tokens, motion, and shared interaction patterns. The registry runs every guard listed here; an app supplies\n// each one's facts under its id.\n\nimport type { GenericGuardDefinition } from '../../node/suite/config.js';\nimport {\n accessibleControlsGuard,\n wholeInputNumbersGuard,\n type AccessibleControlFacts,\n type WholeInputNumberFacts,\n} from './accessibility.js';\nimport {\n stylesheetEntryGuard,\n tailwindSourcesGuard,\n themeTokensGuard,\n utilityCollisionGuard,\n type StylesheetEntryFacts,\n type TailwindSourceFacts,\n type ThemeTokenFacts,\n} from './cascade.js';\nimport {\n enumLabelsGuard,\n schemaBoundsGuard,\n textEntryGuard,\n type EnumLabelFacts,\n type SchemaBoundFacts,\n type TextEntryFacts,\n} from './definitions.js';\nimport {\n layoutOrderGuard,\n loadingFeedbackGuard,\n navigationGuard,\n pendingNameSeatGuard,\n showMoreGuard,\n type LayoutOrderFacts,\n type LoadingFeedbackFacts,\n type NavigationFacts,\n type PendingNameSeatFacts,\n type ShowMoreFacts,\n} from './interaction.js';\nimport { selfHostedFontsGuard } from './fonts.js';\nimport { motionGuard, type MotionFacts } from './motion.js';\nimport {\n colorLiteralsGuard,\n scaleClassesGuard,\n shadowOwnershipGuard,\n stackRowsGuard,\n statusForegroundGuard,\n unavailableAffordanceGuard,\n type ColorLiteralFacts,\n type EntryStylesheetFacts,\n} from './styling.js';\n\n/** The facts each guard in this area needs, by its id. */\nexport interface UiGuardFacts {\n colorLiterals: ColorLiteralFacts;\n themeScaleClasses: EntryStylesheetFacts;\n tailwindSources: TailwindSourceFacts;\n stylesheetEntry: StylesheetEntryFacts;\n themeTokens: ThemeTokenFacts;\n motionTokens: MotionFacts;\n shadowOwnership: Record<string, never>;\n statusForegroundOnFill: Record<string, never>;\n semanticTextCollision: EntryStylesheetFacts;\n stackOnRows: Record<string, never>;\n unavailableControlAffordance: Record<string, never>;\n navigationHelper: NavigationFacts;\n loadingFeedback: LoadingFeedbackFacts;\n rootLayoutOrder: LayoutOrderFacts;\n showMoreToggle: ShowMoreFacts;\n enumLabels: EnumLabelFacts;\n textEntryBoxes: TextEntryFacts;\n schemaBounds: SchemaBoundFacts;\n pendingNameSeat: PendingNameSeatFacts;\n accessibleControls: AccessibleControlFacts;\n wholeInputNumbers: WholeInputNumberFacts;\n selfHostedFonts: Record<string, never>;\n}\n\nexport const UI_GUARDS: ReadonlyArray<GenericGuardDefinition<never>> = Object.freeze([\n colorLiteralsGuard,\n scaleClassesGuard,\n tailwindSourcesGuard,\n stylesheetEntryGuard,\n themeTokensGuard,\n motionGuard,\n shadowOwnershipGuard,\n statusForegroundGuard,\n utilityCollisionGuard,\n stackRowsGuard,\n unavailableAffordanceGuard,\n navigationGuard,\n loadingFeedbackGuard,\n layoutOrderGuard,\n showMoreGuard,\n enumLabelsGuard,\n textEntryGuard,\n schemaBoundsGuard,\n pendingNameSeatGuard,\n accessibleControlsGuard,\n wholeInputNumbersGuard,\n selfHostedFontsGuard,\n] as ReadonlyArray<GenericGuardDefinition<never>>);\n"]}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type BuildSecretSkip } from './apphosting-env.js';
|
|
2
|
+
export declare function runAppHostingBuild({ repoRoot, skips, argv, }: {
|
|
3
|
+
repoRoot: string;
|
|
4
|
+
skips: readonly BuildSecretSkip[];
|
|
5
|
+
argv?: readonly string[];
|
|
6
|
+
}): void;
|
|
7
|
+
//# sourceMappingURL=apphosting-build.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apphosting-build.d.ts","sourceRoot":"","sources":["../../../src/node/tooling/apphosting-build.ts"],"names":[],"mappings":"AAMA,OAAO,EAIL,KAAK,eAAe,EACrB,MAAM,qBAAqB,CAAC;AAG7B,wBAAgB,kBAAkB,CAAC,EACjC,QAAQ,EACR,KAAK,EACL,IAA4B,GAC7B,EAAE;IACD,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,SAAS,eAAe,EAAE,CAAC;IAClC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1B,GAAG,IAAI,CAyBP"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// The environment-parity production build: derive the App Hosting BUILD environment for
|
|
2
|
+
// `--environment <develop|production>` and run `npm run build` with it, or with `--print-only`
|
|
3
|
+
// stop after the derivation (it fails closed exactly as the build would, without compiling).
|
|
4
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { AppHostingConfigError, buildEnvFromConfigs, configFilesFor, } from './apphosting-env.js';
|
|
7
|
+
import { runNpmScript } from './shell.js';
|
|
8
|
+
export function runAppHostingBuild({ repoRoot, skips, argv = process.argv.slice(2), }) {
|
|
9
|
+
const at = argv.indexOf('--environment');
|
|
10
|
+
const environment = (at !== -1 ? (argv[at + 1] ?? '') : '').trim();
|
|
11
|
+
let env;
|
|
12
|
+
try {
|
|
13
|
+
const configs = configFilesFor(environment).map((rel) => {
|
|
14
|
+
const abs = path.join(repoRoot, rel);
|
|
15
|
+
if (!existsSync(abs))
|
|
16
|
+
throw new AppHostingConfigError(`${rel}: not found`);
|
|
17
|
+
return { label: rel, text: readFileSync(abs, 'utf8') };
|
|
18
|
+
});
|
|
19
|
+
env = buildEnvFromConfigs(configs, skips);
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (error instanceof AppHostingConfigError) {
|
|
23
|
+
console.error(`::error::apphosting-build-env: ${error.message}`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
// NAMES only: a build-time value is not a secret, but the export step never prints values.
|
|
29
|
+
console.log(`apphosting-build-env: ${environment} exports ${Object.keys(env).length} BUILD variable(s): ${Object.keys(env).sort().join(', ')}`);
|
|
30
|
+
if (argv.includes('--print-only'))
|
|
31
|
+
return;
|
|
32
|
+
process.exit(runNpmScript('build', { cwd: repoRoot, env: { ...process.env, ...env } }));
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=apphosting-build.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apphosting-build.js","sourceRoot":"","sources":["../../../src/node/tooling/apphosting-build.ts"],"names":[],"mappings":"AAAA,wFAAwF;AACxF,+FAA+F;AAC/F,6FAA6F;AAE7F,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EACL,qBAAqB,EACrB,mBAAmB,EACnB,cAAc,GAEf,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE1C,MAAM,UAAU,kBAAkB,CAAC,EACjC,QAAQ,EACR,KAAK,EACL,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAK7B;IACC,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;IACzC,MAAM,WAAW,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACnE,IAAI,GAA2B,CAAC;IAChC,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,cAAc,CAAC,WAAW,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;YACtD,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;YACrC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;gBAAE,MAAM,IAAI,qBAAqB,CAAC,GAAG,GAAG,aAAa,CAAC,CAAC;YAC3E,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,YAAY,CAAC,GAAG,EAAE,MAAM,CAAC,EAAE,CAAC;QACzD,CAAC,CAAC,CAAC;QACH,GAAG,GAAG,mBAAmB,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IAC5C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,qBAAqB,EAAE,CAAC;YAC3C,OAAO,CAAC,KAAK,CAAC,kCAAkC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;YACjE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QACD,MAAM,KAAK,CAAC;IACd,CAAC;IAED,2FAA2F;IAC3F,OAAO,CAAC,GAAG,CACT,yBAAyB,WAAW,YAAY,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,uBAAuB,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CACnI,CAAC;IACF,IAAI,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC;QAAE,OAAO;IAC1C,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,OAAO,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,GAAG,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC;AAC1F,CAAC","sourcesContent":["// The environment-parity production build: derive the App Hosting BUILD environment for\n// `--environment <develop|production>` and run `npm run build` with it, or with `--print-only`\n// stop after the derivation (it fails closed exactly as the build would, without compiling).\n\nimport { existsSync, readFileSync } from 'node:fs';\nimport path from 'node:path';\nimport {\n AppHostingConfigError,\n buildEnvFromConfigs,\n configFilesFor,\n type BuildSecretSkip,\n} from './apphosting-env.js';\nimport { runNpmScript } from './shell.js';\n\nexport function runAppHostingBuild({\n repoRoot,\n skips,\n argv = process.argv.slice(2),\n}: {\n repoRoot: string;\n skips: readonly BuildSecretSkip[];\n argv?: readonly string[];\n}): void {\n const at = argv.indexOf('--environment');\n const environment = (at !== -1 ? (argv[at + 1] ?? '') : '').trim();\n let env: Record<string, string>;\n try {\n const configs = configFilesFor(environment).map((rel) => {\n const abs = path.join(repoRoot, rel);\n if (!existsSync(abs)) throw new AppHostingConfigError(`${rel}: not found`);\n return { label: rel, text: readFileSync(abs, 'utf8') };\n });\n env = buildEnvFromConfigs(configs, skips);\n } catch (error) {\n if (error instanceof AppHostingConfigError) {\n console.error(`::error::apphosting-build-env: ${error.message}`);\n process.exit(1);\n }\n throw error;\n }\n\n // NAMES only: a build-time value is not a secret, but the export step never prints values.\n console.log(\n `apphosting-build-env: ${environment} exports ${Object.keys(env).length} BUILD variable(s): ${Object.keys(env).sort().join(', ')}`,\n );\n if (argv.includes('--print-only')) return;\n process.exit(runNpmScript('build', { cwd: repoRoot, env: { ...process.env, ...env } }));\n}\n"]}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export declare const BASE_CONFIG = "apphosting.yaml";
|
|
2
|
+
export declare const PRODUCTION_CONFIG = "apphosting.production.yaml";
|
|
3
|
+
export declare class AppHostingConfigError extends Error {
|
|
4
|
+
}
|
|
5
|
+
/** A BUILD secret the parity build may proceed without, and why its absence changes nothing. */
|
|
6
|
+
export interface BuildSecretSkip {
|
|
7
|
+
variable: string;
|
|
8
|
+
why: string;
|
|
9
|
+
}
|
|
10
|
+
export interface AppHostingEnvEntry {
|
|
11
|
+
variable: string;
|
|
12
|
+
availability?: unknown;
|
|
13
|
+
value?: unknown;
|
|
14
|
+
secret?: unknown;
|
|
15
|
+
}
|
|
16
|
+
/** App Hosting's default when `availability` is omitted is BUILD + RUNTIME. */
|
|
17
|
+
export declare function isBuildAvailable(entry: AppHostingEnvEntry): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* One config file's `env` entries. A duplicate `variable` inside one file is an error: the keyed
|
|
20
|
+
* merge applies BETWEEN the base and the overlay, and an ambiguous duplicate has no winner.
|
|
21
|
+
*/
|
|
22
|
+
export declare function parseConfig(text: string, label: string): AppHostingEnvEntry[];
|
|
23
|
+
/** Keyed merge: an overlay entry REPLACES the base entry of the same variable, order kept. */
|
|
24
|
+
export declare function mergeEnv(baseEntries: readonly AppHostingEnvEntry[], overlayEntries: readonly AppHostingEnvEntry[]): AppHostingEnvEntry[];
|
|
25
|
+
/**
|
|
26
|
+
* The BUILD-available environment from the ordered config files (`[base]` for develop,
|
|
27
|
+
* `[base, overlay]` for production). RUNTIME-only entries are left out. Throws when any
|
|
28
|
+
* BUILD-available entry is a secret reference not in `skips`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function buildEnvFromConfigs(configs: readonly {
|
|
31
|
+
text: string;
|
|
32
|
+
label: string;
|
|
33
|
+
}[], skips: readonly BuildSecretSkip[]): Record<string, string>;
|
|
34
|
+
/** The config files an environment reads, in merge order. */
|
|
35
|
+
export declare function configFilesFor(environment: string): string[];
|
|
36
|
+
//# sourceMappingURL=apphosting-env.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apphosting-env.d.ts","sourceRoot":"","sources":["../../../src/node/tooling/apphosting-env.ts"],"names":[],"mappings":"AAaA,eAAO,MAAM,WAAW,oBAAoB,CAAC;AAC7C,eAAO,MAAM,iBAAiB,+BAA+B,CAAC;AAE9D,qBAAa,qBAAsB,SAAQ,KAAK;CAAG;AAEnD,gGAAgG;AAChG,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,+EAA+E;AAC/E,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAMnE;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,kBAAkB,EAAE,CA0C7E;AAED,8FAA8F;AAC9F,wBAAgB,QAAQ,CACtB,WAAW,EAAE,SAAS,kBAAkB,EAAE,EAC1C,cAAc,EAAE,SAAS,kBAAkB,EAAE,GAC5C,kBAAkB,EAAE,CAKtB;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,SAAS;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EAAE,EACnD,KAAK,EAAE,SAAS,eAAe,EAAE,GAChC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAuBxB;AAED,6DAA6D;AAC7D,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,CAM5D"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// The App Hosting BUILD environment, derived from the same committed `apphosting*.yaml` files App
|
|
2
|
+
// Hosting reads, so a CI parity build carries exactly the build inputs the real rollout bakes into
|
|
3
|
+
// the bundle — never a second copy of the values in workflow YAML.
|
|
4
|
+
//
|
|
5
|
+
// Merge semantics mirror App Hosting: production reads the base file AND its overlay, and the
|
|
6
|
+
// overlay wins per keyed variable.
|
|
7
|
+
//
|
|
8
|
+
// Fails closed: a BUILD-available `secret:` reference cannot be resolved in CI, so its presence is
|
|
9
|
+
// an error, not a silently missing build input. The only exceptions are the app's named skips, each
|
|
10
|
+
// justified as absence-non-fatal.
|
|
11
|
+
import { parse as parseYaml } from 'yaml';
|
|
12
|
+
export const BASE_CONFIG = 'apphosting.yaml';
|
|
13
|
+
export const PRODUCTION_CONFIG = 'apphosting.production.yaml';
|
|
14
|
+
export class AppHostingConfigError extends Error {
|
|
15
|
+
}
|
|
16
|
+
/** App Hosting's default when `availability` is omitted is BUILD + RUNTIME. */
|
|
17
|
+
export function isBuildAvailable(entry) {
|
|
18
|
+
if (entry.availability === undefined || entry.availability === null)
|
|
19
|
+
return true;
|
|
20
|
+
if (!Array.isArray(entry.availability)) {
|
|
21
|
+
throw new AppHostingConfigError(`${entry.variable}: availability must be a list`);
|
|
22
|
+
}
|
|
23
|
+
return entry.availability.includes('BUILD');
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* One config file's `env` entries. A duplicate `variable` inside one file is an error: the keyed
|
|
27
|
+
* merge applies BETWEEN the base and the overlay, and an ambiguous duplicate has no winner.
|
|
28
|
+
*/
|
|
29
|
+
export function parseConfig(text, label) {
|
|
30
|
+
let doc;
|
|
31
|
+
try {
|
|
32
|
+
doc = parseYaml(text);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
throw new AppHostingConfigError(`${label}: not valid YAML (${error instanceof Error ? error.message : String(error)})`);
|
|
36
|
+
}
|
|
37
|
+
if (doc === null || doc === undefined)
|
|
38
|
+
return [];
|
|
39
|
+
if (typeof doc !== 'object' || Array.isArray(doc)) {
|
|
40
|
+
throw new AppHostingConfigError(`${label}: top level must be a mapping`);
|
|
41
|
+
}
|
|
42
|
+
const env = doc.env;
|
|
43
|
+
if (env === undefined || env === null)
|
|
44
|
+
return [];
|
|
45
|
+
if (!Array.isArray(env))
|
|
46
|
+
throw new AppHostingConfigError(`${label}: 'env' must be a list`);
|
|
47
|
+
const seen = new Set();
|
|
48
|
+
return env.map((raw) => {
|
|
49
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
50
|
+
throw new AppHostingConfigError(`${label}: every 'env' entry must be a mapping`);
|
|
51
|
+
}
|
|
52
|
+
const { variable, value, secret, availability } = raw;
|
|
53
|
+
if (typeof variable !== 'string' || variable === '') {
|
|
54
|
+
throw new AppHostingConfigError(`${label}: an 'env' entry is missing 'variable'`);
|
|
55
|
+
}
|
|
56
|
+
if (seen.has(variable)) {
|
|
57
|
+
throw new AppHostingConfigError(`${label}: duplicate 'variable' ${variable} within one file`);
|
|
58
|
+
}
|
|
59
|
+
seen.add(variable);
|
|
60
|
+
const hasValue = value !== undefined;
|
|
61
|
+
const hasSecret = secret !== undefined;
|
|
62
|
+
if (hasValue === hasSecret) {
|
|
63
|
+
throw new AppHostingConfigError(`${label}: ${variable} must declare exactly one of 'value' or 'secret'`);
|
|
64
|
+
}
|
|
65
|
+
if (hasValue && (typeof value === 'object' || typeof value === 'function')) {
|
|
66
|
+
throw new AppHostingConfigError(`${label}: ${variable} has a non-scalar 'value'`);
|
|
67
|
+
}
|
|
68
|
+
return { variable, availability, ...(hasValue ? { value } : { secret }) };
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/** Keyed merge: an overlay entry REPLACES the base entry of the same variable, order kept. */
|
|
72
|
+
export function mergeEnv(baseEntries, overlayEntries) {
|
|
73
|
+
const merged = new Map();
|
|
74
|
+
for (const entry of baseEntries)
|
|
75
|
+
merged.set(entry.variable, entry);
|
|
76
|
+
for (const entry of overlayEntries)
|
|
77
|
+
merged.set(entry.variable, entry);
|
|
78
|
+
return [...merged.values()];
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The BUILD-available environment from the ordered config files (`[base]` for develop,
|
|
82
|
+
* `[base, overlay]` for production). RUNTIME-only entries are left out. Throws when any
|
|
83
|
+
* BUILD-available entry is a secret reference not in `skips`.
|
|
84
|
+
*/
|
|
85
|
+
export function buildEnvFromConfigs(configs, skips) {
|
|
86
|
+
const skipped = new Set(skips.map((skip) => skip.variable));
|
|
87
|
+
const merged = configs.reduce((acc, cfg) => mergeEnv(acc, parseConfig(cfg.text, cfg.label)), []);
|
|
88
|
+
const env = {};
|
|
89
|
+
const buildSecrets = [];
|
|
90
|
+
for (const entry of merged) {
|
|
91
|
+
if (!isBuildAvailable(entry))
|
|
92
|
+
continue;
|
|
93
|
+
if (entry.secret !== undefined) {
|
|
94
|
+
if (!skipped.has(entry.variable))
|
|
95
|
+
buildSecrets.push(entry.variable);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
env[entry.variable] = String(entry.value);
|
|
99
|
+
}
|
|
100
|
+
if (buildSecrets.length) {
|
|
101
|
+
throw new AppHostingConfigError(`BUILD-available secret reference(s) cannot be resolved in CI: ${buildSecrets.join(', ')}. ` +
|
|
102
|
+
'A build missing them is not environment parity — move them to RUNTIME availability or supply them explicitly.');
|
|
103
|
+
}
|
|
104
|
+
return env;
|
|
105
|
+
}
|
|
106
|
+
/** The config files an environment reads, in merge order. */
|
|
107
|
+
export function configFilesFor(environment) {
|
|
108
|
+
if (environment === 'develop' || environment === 'development')
|
|
109
|
+
return [BASE_CONFIG];
|
|
110
|
+
if (environment === 'production')
|
|
111
|
+
return [BASE_CONFIG, PRODUCTION_CONFIG];
|
|
112
|
+
throw new AppHostingConfigError(`unknown environment '${environment}' (expected 'develop' or 'production')`);
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=apphosting-env.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apphosting-env.js","sourceRoot":"","sources":["../../../src/node/tooling/apphosting-env.ts"],"names":[],"mappings":"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","sourcesContent":["// The App Hosting BUILD environment, derived from the same committed `apphosting*.yaml` files App\n// Hosting reads, so a CI parity build carries exactly the build inputs the real rollout bakes into\n// the bundle — never a second copy of the values in workflow YAML.\n//\n// Merge semantics mirror App Hosting: production reads the base file AND its overlay, and the\n// overlay wins per keyed variable.\n//\n// Fails closed: a BUILD-available `secret:` reference cannot be resolved in CI, so its presence is\n// an error, not a silently missing build input. The only exceptions are the app's named skips, each\n// justified as absence-non-fatal.\n\nimport { parse as parseYaml } from 'yaml';\n\nexport const BASE_CONFIG = 'apphosting.yaml';\nexport const PRODUCTION_CONFIG = 'apphosting.production.yaml';\n\nexport class AppHostingConfigError extends Error {}\n\n/** A BUILD secret the parity build may proceed without, and why its absence changes nothing. */\nexport interface BuildSecretSkip {\n variable: string;\n why: string;\n}\n\nexport interface AppHostingEnvEntry {\n variable: string;\n availability?: unknown;\n value?: unknown;\n secret?: unknown;\n}\n\n/** App Hosting's default when `availability` is omitted is BUILD + RUNTIME. */\nexport function isBuildAvailable(entry: AppHostingEnvEntry): boolean {\n if (entry.availability === undefined || entry.availability === null) return true;\n if (!Array.isArray(entry.availability)) {\n throw new AppHostingConfigError(`${entry.variable}: availability must be a list`);\n }\n return entry.availability.includes('BUILD');\n}\n\n/**\n * One config file's `env` entries. A duplicate `variable` inside one file is an error: the keyed\n * merge applies BETWEEN the base and the overlay, and an ambiguous duplicate has no winner.\n */\nexport function parseConfig(text: string, label: string): AppHostingEnvEntry[] {\n let doc: unknown;\n try {\n doc = parseYaml(text);\n } catch (error) {\n throw new AppHostingConfigError(\n `${label}: not valid YAML (${error instanceof Error ? error.message : String(error)})`,\n );\n }\n if (doc === null || doc === undefined) return [];\n if (typeof doc !== 'object' || Array.isArray(doc)) {\n throw new AppHostingConfigError(`${label}: top level must be a mapping`);\n }\n const env = (doc as { env?: unknown }).env;\n if (env === undefined || env === null) return [];\n if (!Array.isArray(env)) throw new AppHostingConfigError(`${label}: 'env' must be a list`);\n\n const seen = new Set<string>();\n return env.map((raw: unknown) => {\n if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {\n throw new AppHostingConfigError(`${label}: every 'env' entry must be a mapping`);\n }\n const { variable, value, secret, availability } = raw as Record<string, unknown>;\n if (typeof variable !== 'string' || variable === '') {\n throw new AppHostingConfigError(`${label}: an 'env' entry is missing 'variable'`);\n }\n if (seen.has(variable)) {\n throw new AppHostingConfigError(`${label}: duplicate 'variable' ${variable} within one file`);\n }\n seen.add(variable);\n const hasValue = value !== undefined;\n const hasSecret = secret !== undefined;\n if (hasValue === hasSecret) {\n throw new AppHostingConfigError(\n `${label}: ${variable} must declare exactly one of 'value' or 'secret'`,\n );\n }\n if (hasValue && (typeof value === 'object' || typeof value === 'function')) {\n throw new AppHostingConfigError(`${label}: ${variable} has a non-scalar 'value'`);\n }\n return { variable, availability, ...(hasValue ? { value } : { secret }) };\n });\n}\n\n/** Keyed merge: an overlay entry REPLACES the base entry of the same variable, order kept. */\nexport function mergeEnv(\n baseEntries: readonly AppHostingEnvEntry[],\n overlayEntries: readonly AppHostingEnvEntry[],\n): AppHostingEnvEntry[] {\n const merged = new Map<string, AppHostingEnvEntry>();\n for (const entry of baseEntries) merged.set(entry.variable, entry);\n for (const entry of overlayEntries) merged.set(entry.variable, entry);\n return [...merged.values()];\n}\n\n/**\n * The BUILD-available environment from the ordered config files (`[base]` for develop,\n * `[base, overlay]` for production). RUNTIME-only entries are left out. Throws when any\n * BUILD-available entry is a secret reference not in `skips`.\n */\nexport function buildEnvFromConfigs(\n configs: readonly { text: string; label: string }[],\n skips: readonly BuildSecretSkip[],\n): Record<string, string> {\n const skipped = new Set(skips.map((skip) => skip.variable));\n const merged = configs.reduce<AppHostingEnvEntry[]>(\n (acc, cfg) => mergeEnv(acc, parseConfig(cfg.text, cfg.label)),\n [],\n );\n const env: Record<string, string> = {};\n const buildSecrets: string[] = [];\n for (const entry of merged) {\n if (!isBuildAvailable(entry)) continue;\n if (entry.secret !== undefined) {\n if (!skipped.has(entry.variable)) buildSecrets.push(entry.variable);\n continue;\n }\n env[entry.variable] = String(entry.value);\n }\n if (buildSecrets.length) {\n throw new AppHostingConfigError(\n `BUILD-available secret reference(s) cannot be resolved in CI: ${buildSecrets.join(', ')}. ` +\n 'A build missing them is not environment parity — move them to RUNTIME availability or supply them explicitly.',\n );\n }\n return env;\n}\n\n/** The config files an environment reads, in merge order. */\nexport function configFilesFor(environment: string): string[] {\n if (environment === 'develop' || environment === 'development') return [BASE_CONFIG];\n if (environment === 'production') return [BASE_CONFIG, PRODUCTION_CONFIG];\n throw new AppHostingConfigError(\n `unknown environment '${environment}' (expected 'develop' or 'production')`,\n );\n}\n"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** The label the Firebase CLI puts on every `onCall` function's service. */
|
|
2
|
+
export declare const CALLABLE_LABEL_FILTER = "metadata.labels.deployment-callable=true";
|
|
3
|
+
export interface IamPolicy {
|
|
4
|
+
bindings?: {
|
|
5
|
+
role?: string;
|
|
6
|
+
members?: string[];
|
|
7
|
+
}[];
|
|
8
|
+
}
|
|
9
|
+
/** gcloud `value(...)` output as trimmed, `\r`-stripped, non-empty lines. */
|
|
10
|
+
export declare function lines(out: unknown): string[];
|
|
11
|
+
export declare function hasAllUsersInvoker(policy: IamPolicy | null | undefined): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* The verdict over every service checked: which lack the binding, which could not be read, and
|
|
14
|
+
* whether nothing was found at all — a check that read nothing proved nothing, so it fails.
|
|
15
|
+
* `policies` maps a service to its parsed policy, or to an `Error` when it could not be read.
|
|
16
|
+
*/
|
|
17
|
+
export declare function assessInvokers(services: readonly string[], policies: ReadonlyMap<string, IamPolicy | Error>): {
|
|
18
|
+
missing: string[];
|
|
19
|
+
unreadable: string[];
|
|
20
|
+
empty: boolean;
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=callable-invokers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"callable-invokers.d.ts","sourceRoot":"","sources":["../../../src/node/tooling/callable-invokers.ts"],"names":[],"mappings":"AAMA,4EAA4E;AAC5E,eAAO,MAAM,qBAAqB,6CAA6C,CAAC;AAEhF,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,EAAE,CAAC;CACpD;AAED,6EAA6E;AAC7E,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,EAAE,CAK5C;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAKhF;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,SAAS,MAAM,EAAE,EAC3B,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,GAAG,KAAK,CAAC,GAC/C;IAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,UAAU,EAAE,MAAM,EAAE,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAc7D"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Every PUBLIC callable or webhook Cloud Run service must bind `allUsers` to `roles/run.invoker`.
|
|
2
|
+
// Without it the request is refused at the Cloud Run boundary and never reaches the function, so
|
|
3
|
+
// nothing runs, nothing throws, and nothing reaches error tracking. The auth and App Check gates
|
|
4
|
+
// live INSIDE each function; the platform binding has to be public for a request to arrive and be
|
|
5
|
+
// gated at all, so it is never a setting to tighten.
|
|
6
|
+
/** The label the Firebase CLI puts on every `onCall` function's service. */
|
|
7
|
+
export const CALLABLE_LABEL_FILTER = 'metadata.labels.deployment-callable=true';
|
|
8
|
+
/** gcloud `value(...)` output as trimmed, `\r`-stripped, non-empty lines. */
|
|
9
|
+
export function lines(out) {
|
|
10
|
+
return String(out ?? '')
|
|
11
|
+
.split(/\r?\n/)
|
|
12
|
+
.map((line) => line.replace(/\r/g, '').trim())
|
|
13
|
+
.filter(Boolean);
|
|
14
|
+
}
|
|
15
|
+
export function hasAllUsersInvoker(policy) {
|
|
16
|
+
return (policy?.bindings ?? []).some((binding) => binding.role === 'roles/run.invoker' && (binding.members ?? []).includes('allUsers'));
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The verdict over every service checked: which lack the binding, which could not be read, and
|
|
20
|
+
* whether nothing was found at all — a check that read nothing proved nothing, so it fails.
|
|
21
|
+
* `policies` maps a service to its parsed policy, or to an `Error` when it could not be read.
|
|
22
|
+
*/
|
|
23
|
+
export function assessInvokers(services, policies) {
|
|
24
|
+
const missing = [];
|
|
25
|
+
const unreadable = [];
|
|
26
|
+
for (const name of services) {
|
|
27
|
+
const policy = policies.get(name);
|
|
28
|
+
if (policy === undefined || policy instanceof Error) {
|
|
29
|
+
unreadable.push(policy instanceof Error ? `${name} (${policy.message})` : `${name} (no policy read)`);
|
|
30
|
+
}
|
|
31
|
+
else if (!hasAllUsersInvoker(policy)) {
|
|
32
|
+
missing.push(name);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return { missing, unreadable, empty: services.length === 0 };
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=callable-invokers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"callable-invokers.js","sourceRoot":"","sources":["../../../src/node/tooling/callable-invokers.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAClG,iGAAiG;AACjG,iGAAiG;AACjG,kGAAkG;AAClG,qDAAqD;AAErD,4EAA4E;AAC5E,MAAM,CAAC,MAAM,qBAAqB,GAAG,0CAA0C,CAAC;AAMhF,6EAA6E;AAC7E,MAAM,UAAU,KAAK,CAAC,GAAY;IAChC,OAAO,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC;SACrB,KAAK,CAAC,OAAO,CAAC;SACd,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;SAC7C,MAAM,CAAC,OAAO,CAAC,CAAC;AACrB,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,MAAoC;IACrE,OAAO,CAAC,MAAM,EAAE,QAAQ,IAAI,EAAE,CAAC,CAAC,IAAI,CAClC,CAAC,OAAO,EAAE,EAAE,CACV,OAAO,CAAC,IAAI,KAAK,mBAAmB,IAAI,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,CACvF,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAC5B,QAA2B,EAC3B,QAAgD;IAEhD,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,MAAM,UAAU,GAAa,EAAE,CAAC;IAChC,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;QAC5B,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAClC,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,YAAY,KAAK,EAAE,CAAC;YACpD,UAAU,CAAC,IAAI,CACb,MAAM,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,IAAI,KAAK,MAAM,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,mBAAmB,CACrF,CAAC;QACJ,CAAC;aAAM,IAAI,CAAC,kBAAkB,CAAC,MAAM,CAAC,EAAE,CAAC;YACvC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACrB,CAAC;IACH,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;AAC/D,CAAC","sourcesContent":["// Every PUBLIC callable or webhook Cloud Run service must bind `allUsers` to `roles/run.invoker`.\n// Without it the request is refused at the Cloud Run boundary and never reaches the function, so\n// nothing runs, nothing throws, and nothing reaches error tracking. The auth and App Check gates\n// live INSIDE each function; the platform binding has to be public for a request to arrive and be\n// gated at all, so it is never a setting to tighten.\n\n/** The label the Firebase CLI puts on every `onCall` function's service. */\nexport const CALLABLE_LABEL_FILTER = 'metadata.labels.deployment-callable=true';\n\nexport interface IamPolicy {\n bindings?: { role?: string; members?: string[] }[];\n}\n\n/** gcloud `value(...)` output as trimmed, `\\r`-stripped, non-empty lines. */\nexport function lines(out: unknown): string[] {\n return String(out ?? '')\n .split(/\\r?\\n/)\n .map((line) => line.replace(/\\r/g, '').trim())\n .filter(Boolean);\n}\n\nexport function hasAllUsersInvoker(policy: IamPolicy | null | undefined): boolean {\n return (policy?.bindings ?? []).some(\n (binding) =>\n binding.role === 'roles/run.invoker' && (binding.members ?? []).includes('allUsers'),\n );\n}\n\n/**\n * The verdict over every service checked: which lack the binding, which could not be read, and\n * whether nothing was found at all — a check that read nothing proved nothing, so it fails.\n * `policies` maps a service to its parsed policy, or to an `Error` when it could not be read.\n */\nexport function assessInvokers(\n services: readonly string[],\n policies: ReadonlyMap<string, IamPolicy | Error>,\n): { missing: string[]; unreadable: string[]; empty: boolean } {\n const missing: string[] = [];\n const unreadable: string[] = [];\n for (const name of services) {\n const policy = policies.get(name);\n if (policy === undefined || policy instanceof Error) {\n unreadable.push(\n policy instanceof Error ? `${name} (${policy.message})` : `${name} (no policy read)`,\n );\n } else if (!hasAllUsersInvoker(policy)) {\n missing.push(name);\n }\n }\n return { missing, unreadable, empty: services.length === 0 };\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cleanup.d.ts","sourceRoot":"","sources":["../../../src/node/tooling/cleanup.ts"],"names":[],"mappings":"AAOA,wBAAgB,UAAU,CAAC,EACzB,QAAQ,EACR,OAAO,GACR,EAAE;IACD,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5B,GAAG,IAAI,CAeP"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Cross-platform deletion of an app's disposable build and cache output. The targets are a fixed,
|
|
2
|
+
// repository-relative list — never arguments, never globs — so this can only ever delete known
|
|
3
|
+
// output, and a target that resolves outside the repository stops the command.
|
|
4
|
+
import { existsSync, rmSync } from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
export function runCleanup({ repoRoot, targets, }) {
|
|
7
|
+
const root = path.resolve(repoRoot);
|
|
8
|
+
let removed = 0;
|
|
9
|
+
for (const target of targets) {
|
|
10
|
+
const absolute = path.join(root, ...target.split('/'));
|
|
11
|
+
if (!absolute.startsWith(`${root}${path.sep}`)) {
|
|
12
|
+
console.error(`cleanup: refusing to delete outside the repository: ${absolute}`);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
if (existsSync(absolute)) {
|
|
16
|
+
rmSync(absolute, { recursive: true, force: true });
|
|
17
|
+
removed += 1;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
console.log(`cleanup: removed ${removed} path(s); ${targets.length - removed} already absent.`);
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=cleanup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cleanup.js","sourceRoot":"","sources":["../../../src/node/tooling/cleanup.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAClG,+FAA+F;AAC/F,+EAA+E;AAE/E,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAC7C,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,MAAM,UAAU,UAAU,CAAC,EACzB,QAAQ,EACR,OAAO,GAIR;IACC,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACpC,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;QACvD,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC;YAC/C,OAAO,CAAC,KAAK,CAAC,uDAAuD,QAAQ,EAAE,CAAC,CAAC;YACjF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QACD,IAAI,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YACzB,MAAM,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YACnD,OAAO,IAAI,CAAC,CAAC;QACf,CAAC;IACH,CAAC;IACD,OAAO,CAAC,GAAG,CAAC,oBAAoB,OAAO,aAAa,OAAO,CAAC,MAAM,GAAG,OAAO,kBAAkB,CAAC,CAAC;AAClG,CAAC","sourcesContent":["// Cross-platform deletion of an app's disposable build and cache output. The targets are a fixed,\n// repository-relative list — never arguments, never globs — so this can only ever delete known\n// output, and a target that resolves outside the repository stops the command.\n\nimport { existsSync, rmSync } from 'node:fs';\nimport path from 'node:path';\n\nexport function runCleanup({\n repoRoot,\n targets,\n}: {\n repoRoot: string;\n targets: readonly string[];\n}): void {\n const root = path.resolve(repoRoot);\n let removed = 0;\n for (const target of targets) {\n const absolute = path.join(root, ...target.split('/'));\n if (!absolute.startsWith(`${root}${path.sep}`)) {\n console.error(`cleanup: refusing to delete outside the repository: ${absolute}`);\n process.exit(1);\n }\n if (existsSync(absolute)) {\n rmSync(absolute, { recursive: true, force: true });\n removed += 1;\n }\n }\n console.log(`cleanup: removed ${removed} path(s); ${targets.length - removed} already absent.`);\n}\n"]}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type ExpectedCodebase } from './deployed-functions.js';
|
|
2
|
+
/**
|
|
3
|
+
* Apply the checked-in `cors.json` to the project's default Storage bucket, read the bucket's
|
|
4
|
+
* policy back, and throw unless it is exactly that policy.
|
|
5
|
+
*/
|
|
6
|
+
export declare function applyStorageCors(repoRoot: string, projectId: string): void;
|
|
7
|
+
/**
|
|
8
|
+
* Every labelled callable service, plus the app's public webhook services, must bind `allUsers`
|
|
9
|
+
* to `roles/run.invoker`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function checkCallableInvokers({ repoRoot, region, projectId: requested, extraPublicServices, }: {
|
|
12
|
+
repoRoot: string;
|
|
13
|
+
region: string;
|
|
14
|
+
projectId: string | undefined;
|
|
15
|
+
/** Public `onRequest` services the callable label does not mark, named as Cloud Run names them. */
|
|
16
|
+
extraPublicServices?: readonly string[];
|
|
17
|
+
}): void;
|
|
18
|
+
/**
|
|
19
|
+
* The live functions must be exactly the expected ones, each ACTIVE.
|
|
20
|
+
* `expected` answers the codebases this deploy targeted; an empty answer means the deploy
|
|
21
|
+
* targeted no functions. It throws on input it cannot read, which fails the job.
|
|
22
|
+
*/
|
|
23
|
+
export declare function checkDeployedFunctions({ repoRoot, region, projectId: requested, expected, }: {
|
|
24
|
+
repoRoot: string;
|
|
25
|
+
region: string;
|
|
26
|
+
projectId: string | undefined;
|
|
27
|
+
expected: () => readonly ExpectedCodebase[];
|
|
28
|
+
}): void;
|
|
29
|
+
//# sourceMappingURL=cloud-checks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cloud-checks.d.ts","sourceRoot":"","sources":["../../../src/node/tooling/cloud-checks.ts"],"names":[],"mappings":"AAaA,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,yBAAyB,CAAC;AAuCjC;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CA0B1E;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,EACpC,QAAQ,EACR,MAAM,EACN,SAAS,EAAE,SAAS,EACpB,mBAAwB,GACzB,EAAE;IACD,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,mGAAmG;IACnG,mBAAmB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACzC,GAAG,IAAI,CAwEP;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,QAAQ,EACR,MAAM,EACN,SAAS,EAAE,SAAS,EACpB,QAAQ,GACT,EAAE;IACD,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,QAAQ,EAAE,MAAM,SAAS,gBAAgB,EAAE,CAAC;CAC7C,GAAG,IAAI,CA0DP"}
|