@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@everystack/cli",
3
- "version": "0.2.21",
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.ts"
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 — Shared env config for SST infrastructure.
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
- * import { loadEnvConfig } from '@everystack/cli/sst';
9
- * const env = loadEnvConfig();
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
- export { loadEnvConfig, type EnvConfig } from './env.js';
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
+ }