@everystack/cli 0.2.21 → 0.2.23
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/package.json +3 -2
- package/src/env.js +154 -0
- package/src/env.ts +2 -0
- package/src/sst.ts +75 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@everystack/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.23",
|
|
4
4
|
"description": "CLI and OTA updates for Expo apps on everystack",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"publishConfig": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"./env": {
|
|
35
35
|
"types": "./src/env.ts",
|
|
36
|
-
"default": "./src/env.
|
|
36
|
+
"default": "./src/env.js"
|
|
37
37
|
},
|
|
38
38
|
"./sst": {
|
|
39
39
|
"types": "./src/sst.ts",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"scripts": {
|
|
47
47
|
"test": "jest",
|
|
48
48
|
"build": "tsc --build",
|
|
49
|
+
"prepublishOnly": "tsc --module commonjs --moduleResolution node --target ES2022 --esModuleInterop --skipLibCheck --declaration false --outDir src src/env.ts",
|
|
49
50
|
"lint": "tsc --noEmit"
|
|
50
51
|
},
|
|
51
52
|
"dependencies": {
|
package/src/env.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @everystack/cli/env — Deny-by-default environment variable filtering.
|
|
4
|
+
*
|
|
5
|
+
* One import in app.config.js. Auto-discovers env.config.js.
|
|
6
|
+
* Detects build surface, filters secrets by tier, sets EXPO_PUBLIC_* vars.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* const { extra } = require('@everystack/cli/env').load();
|
|
10
|
+
* module.exports = { expo: { extra } };
|
|
11
|
+
*
|
|
12
|
+
* With explicit config path:
|
|
13
|
+
* const { extra } = require('@everystack/cli/env').load({ path: './env.config' });
|
|
14
|
+
*
|
|
15
|
+
* Multiple stages. Four buckets. One gate. One file.
|
|
16
|
+
*
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
19
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
20
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
21
|
+
};
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.detectSurface = detectSurface;
|
|
24
|
+
exports.loadEnvConfig = loadEnvConfig;
|
|
25
|
+
exports.filterEnv = filterEnv;
|
|
26
|
+
exports.setExpoPublicVars = setExpoPublicVars;
|
|
27
|
+
exports.load = load;
|
|
28
|
+
const path_1 = __importDefault(require("path"));
|
|
29
|
+
const fs_1 = __importDefault(require("fs"));
|
|
30
|
+
/**
|
|
31
|
+
* Detect the current build surface from environment signals.
|
|
32
|
+
*
|
|
33
|
+
* EAS_BUILD / EAS_BUILD_PLATFORM -> native (EAS native build)
|
|
34
|
+
* EAS_UPDATE -> native (EAS OTA update)
|
|
35
|
+
* EVERYSTACK_UPDATE -> web (everystack update)
|
|
36
|
+
* none of the above -> local (pnpm dev)
|
|
37
|
+
*/
|
|
38
|
+
function detectSurface() {
|
|
39
|
+
if (process.env.EAS_BUILD || process.env.EAS_BUILD_PLATFORM || process.env.EAS_UPDATE) {
|
|
40
|
+
return 'native';
|
|
41
|
+
}
|
|
42
|
+
if (process.env.EVERYSTACK_UPDATE) {
|
|
43
|
+
return 'web';
|
|
44
|
+
}
|
|
45
|
+
return 'local';
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Load env config from a file path or auto-discover env.config.js in cwd.
|
|
49
|
+
*/
|
|
50
|
+
function loadEnvConfig(configPath) {
|
|
51
|
+
const resolved = configPath
|
|
52
|
+
? path_1.default.resolve(configPath)
|
|
53
|
+
: findEnvConfig();
|
|
54
|
+
// Synchronous require — same pattern used throughout the CLI package.
|
|
55
|
+
// Works in Metro (CJS interop), tsx (SST), and Node CJS.
|
|
56
|
+
delete require.cache?.[resolved];
|
|
57
|
+
const mod = require(resolved);
|
|
58
|
+
return mod.default || mod;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Auto-discover env.config.js in the current working directory.
|
|
62
|
+
*/
|
|
63
|
+
function findEnvConfig() {
|
|
64
|
+
const name = 'env.config.js';
|
|
65
|
+
const fullPath = path_1.default.resolve(process.cwd(), name);
|
|
66
|
+
if (fs_1.default.existsSync(fullPath)) {
|
|
67
|
+
return fullPath;
|
|
68
|
+
}
|
|
69
|
+
throw new Error('No env.config.js found. Create one:\n\n'
|
|
70
|
+
+ ' module.exports = {\n'
|
|
71
|
+
+ ' server: [],\n'
|
|
72
|
+
+ ' build: [],\n'
|
|
73
|
+
+ ' mobile: [],\n'
|
|
74
|
+
+ ' web: [],\n'
|
|
75
|
+
+ ' };\n');
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Filter process.env through tier allowlists based on the detected surface.
|
|
79
|
+
*
|
|
80
|
+
* Deny-by-default: undeclared keys are dropped.
|
|
81
|
+
* Server and build tiers are NEVER passed through, even if duplicated in other tiers.
|
|
82
|
+
*
|
|
83
|
+
* Surface behavior:
|
|
84
|
+
* local -> web + mobile tiers pass
|
|
85
|
+
* native -> web + mobile tiers pass
|
|
86
|
+
* web -> web tier only
|
|
87
|
+
*/
|
|
88
|
+
function filterEnv(surface, config) {
|
|
89
|
+
// Build the allowlist: web tier always passes
|
|
90
|
+
const allowed = new Set(config.web || []);
|
|
91
|
+
// Mobile tier: passes on native and local, blocked on web
|
|
92
|
+
if (surface !== 'web') {
|
|
93
|
+
for (const k of (config.mobile || [])) {
|
|
94
|
+
allowed.add(k);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// server and build tiers are NEVER added to the allowlist.
|
|
98
|
+
// Actively remove them in case a key appears in multiple tiers.
|
|
99
|
+
const blocked = new Set([
|
|
100
|
+
...(config.server || []),
|
|
101
|
+
...(config.build || []),
|
|
102
|
+
]);
|
|
103
|
+
for (const k of blocked) {
|
|
104
|
+
allowed.delete(k);
|
|
105
|
+
}
|
|
106
|
+
// Collect values for allowed keys from process.env
|
|
107
|
+
const result = {};
|
|
108
|
+
for (const key of allowed) {
|
|
109
|
+
if (process.env[key] !== undefined) {
|
|
110
|
+
result[key] = process.env[key];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Set EXPO_PUBLIC_* process.env vars for web delivery.
|
|
117
|
+
*
|
|
118
|
+
* Constants.expoConfig.extra does NOT work on web — Metro never sets
|
|
119
|
+
* APP_MANIFEST, so Constants.expoConfig is null in the browser. The only
|
|
120
|
+
* way to deliver env values to web client code is via EXPO_PUBLIC_* vars,
|
|
121
|
+
* which Metro inlines as literal strings at compile time.
|
|
122
|
+
*
|
|
123
|
+
* Sets EXPO_PUBLIC_* for ALL filtered keys so consumers can use either
|
|
124
|
+
* delivery path (extra on native, EXPO_PUBLIC_* on web).
|
|
125
|
+
*/
|
|
126
|
+
function setExpoPublicVars(filtered) {
|
|
127
|
+
for (const [key, value] of Object.entries(filtered)) {
|
|
128
|
+
process.env[`EXPO_PUBLIC_${key}`] = value;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Load and filter environment variables for the current build surface.
|
|
133
|
+
*
|
|
134
|
+
* Auto-discovers env.config.js in the current working directory,
|
|
135
|
+
* or accepts an explicit path. Detects the build surface, filters
|
|
136
|
+
* process.env through the tier allowlists, sets EXPO_PUBLIC_* vars,
|
|
137
|
+
* and returns the filtered object for expo.extra.
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* // app.config.js — auto-discover env.config.js
|
|
141
|
+
* const { extra } = require('@everystack/cli/env').load();
|
|
142
|
+
* module.exports = { expo: { extra } };
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* // app.config.js — explicit path
|
|
146
|
+
* const { extra } = require('@everystack/cli/env').load({ path: './env.config' });
|
|
147
|
+
*/
|
|
148
|
+
function load(options) {
|
|
149
|
+
const config = loadEnvConfig(options?.path);
|
|
150
|
+
const surface = detectSurface();
|
|
151
|
+
const filtered = filterEnv(surface, config);
|
|
152
|
+
setExpoPublicVars(filtered);
|
|
153
|
+
return { extra: filtered };
|
|
154
|
+
}
|
package/src/env.ts
CHANGED
|
@@ -37,6 +37,8 @@ export interface EnvConfig {
|
|
|
37
37
|
mobile?: string[];
|
|
38
38
|
/** Safe for all client bundles. Gets EXPO_PUBLIC_* for web delivery. */
|
|
39
39
|
web?: string[];
|
|
40
|
+
/** SST secret declarations (infrastructure). Falls back to server tier if omitted. */
|
|
41
|
+
secrets?: string[];
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
/** Build surface — determines which tiers pass through the filter. */
|
package/src/sst.ts
CHANGED
|
@@ -1,15 +1,85 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @everystack/cli/sst —
|
|
2
|
+
* @everystack/cli/sst — SST infrastructure helpers.
|
|
3
3
|
*
|
|
4
4
|
* Reads the same env.config.js that @everystack/cli/env uses,
|
|
5
5
|
* ensuring sst.config.ts and app.config.js stay in sync.
|
|
6
6
|
*
|
|
7
7
|
* Usage in sst.config.ts:
|
|
8
|
-
*
|
|
9
|
-
* const
|
|
10
|
-
* // env.server = ['DATABASE_URL', 'JWT_SECRET']
|
|
8
|
+
* const { createSecrets } = await import('@everystack/cli/sst');
|
|
9
|
+
* const { secrets, link, environment } = createSecrets(sst.Secret);
|
|
11
10
|
*
|
|
12
11
|
* @module
|
|
13
12
|
*/
|
|
14
13
|
|
|
15
|
-
|
|
14
|
+
import { loadEnvConfig, type EnvConfig } from './env.js';
|
|
15
|
+
|
|
16
|
+
export { loadEnvConfig, type EnvConfig };
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Minimal interface matching the subset of sst.Secret we use.
|
|
20
|
+
*
|
|
21
|
+
* The real sst.Secret is a global available only inside sst.config.ts's
|
|
22
|
+
* run() function. We accept it as a constructor parameter to avoid
|
|
23
|
+
* coupling this package to SST internals.
|
|
24
|
+
*/
|
|
25
|
+
interface SecretInstance {
|
|
26
|
+
value: unknown;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Return type of createSecrets. */
|
|
30
|
+
export interface CreateSecretsResult {
|
|
31
|
+
/** All secrets keyed by env var name for selective access. */
|
|
32
|
+
secrets: Record<string, SecretInstance>;
|
|
33
|
+
/** Flat array of all secret objects for Lambda `link`. */
|
|
34
|
+
link: SecretInstance[];
|
|
35
|
+
/** { KEY: secret.value } map for Lambda `environment`. */
|
|
36
|
+
environment: Record<string, unknown>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Create SST Secret objects from env.config.js.
|
|
41
|
+
*
|
|
42
|
+
* Reads the `secrets` field from env.config.js (falls back to `server`
|
|
43
|
+
* tier if omitted). Creates one SST Secret per declared key name.
|
|
44
|
+
*
|
|
45
|
+
* @param SecretConstructor - The sst.Secret class, passed from run()
|
|
46
|
+
* @param configPath - Optional path to env.config.js (auto-discovers if omitted)
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* const { createSecrets } = await import('@everystack/cli/sst');
|
|
50
|
+
* const { secrets, link, environment } = createSecrets(sst.Secret);
|
|
51
|
+
*
|
|
52
|
+
* // API Lambda — all secrets
|
|
53
|
+
* new sst.aws.Function('Api', {
|
|
54
|
+
* link: [...link, database, media],
|
|
55
|
+
* environment: { ENVIRONMENT: $app.stage, ...environment },
|
|
56
|
+
* });
|
|
57
|
+
*
|
|
58
|
+
* // Worker Lambda — selective
|
|
59
|
+
* new sst.aws.Function('Worker', {
|
|
60
|
+
* link: [secrets.DATABASE_URL, secrets.JWT_SECRET],
|
|
61
|
+
* });
|
|
62
|
+
*
|
|
63
|
+
* // CloudFront edge auth
|
|
64
|
+
* secrets.JWT_SECRET.value.apply((secret) => authFeature({ secret }));
|
|
65
|
+
*/
|
|
66
|
+
export function createSecrets(
|
|
67
|
+
SecretConstructor: new (name: string) => SecretInstance,
|
|
68
|
+
configPath?: string,
|
|
69
|
+
): CreateSecretsResult {
|
|
70
|
+
const config = loadEnvConfig(configPath);
|
|
71
|
+
const keys = [...new Set(config.secrets ?? config.server ?? [])];
|
|
72
|
+
|
|
73
|
+
const secrets: Record<string, SecretInstance> = {};
|
|
74
|
+
const link: SecretInstance[] = [];
|
|
75
|
+
const environment: Record<string, unknown> = {};
|
|
76
|
+
|
|
77
|
+
for (const key of keys) {
|
|
78
|
+
const secret = new SecretConstructor(key);
|
|
79
|
+
secrets[key] = secret;
|
|
80
|
+
link.push(secret);
|
|
81
|
+
environment[key] = secret.value;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { secrets, link, environment };
|
|
85
|
+
}
|