@everystack/cli 0.2.39 → 0.2.40

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/README.md CHANGED
@@ -103,6 +103,33 @@ const storage = createStorage({
103
103
  });
104
104
  ```
105
105
 
106
+ The S3 client is **bounded by default** so a single call can never hang: a
107
+ connect timeout (3s), a per-request timeout (10s), bounded retries (3 attempts),
108
+ and a capped `list()` (paged with a page backstop). This matters most for the
109
+ observability rollup — an unbounded client under an S3 503 storm blocks until
110
+ the Lambda's own timeout, and each hung invocation pins a concurrency slot, so
111
+ it can drain the account pool and starve the app it observes.
112
+
113
+ Tune per-field via `options`, or override the timeouts/retries by env var
114
+ without a code change (env is handy for turning a knob on a live Lambda):
115
+
116
+ ```typescript
117
+ const storage = createStorage({
118
+ type: 's3',
119
+ bucket: 'my-updates-bucket',
120
+ region: 'us-east-1',
121
+ options: {
122
+ connectionTimeoutMs: 3000, // env: EVERYSTACK_S3_CONNECT_TIMEOUT_MS
123
+ requestTimeoutMs: 10000, // env: EVERYSTACK_S3_REQUEST_TIMEOUT_MS
124
+ maxAttempts: 3, // env: EVERYSTACK_S3_MAX_ATTEMPTS
125
+ listMaxKeys: 1000, // keys per ListObjectsV2 page
126
+ listMaxPages: 1000, // hard backstop before list() truncates (warns)
127
+ },
128
+ });
129
+ ```
130
+
131
+ Precedence per field: explicit `options` > env var > default.
132
+
106
133
  ### Custom Adapter
107
134
 
108
135
  Implement the `StorageAdapter` interface:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@everystack/cli",
3
- "version": "0.2.39",
3
+ "version": "0.2.40",
4
4
  "description": "CLI and OTA updates for Expo apps on everystack",
5
5
  "license": "AGPL-3.0-only",
6
6
  "author": "Scalable Technology, Inc. <licensing@scalable.technology>",
@@ -53,6 +53,12 @@
53
53
  "bin": {
54
54
  "everystack": "./src/cli/index.ts"
55
55
  },
56
+ "scripts": {
57
+ "test": "jest",
58
+ "build": "tsc --build",
59
+ "prepublishOnly": "tsc --module commonjs --moduleResolution node --target ES2022 --esModuleInterop --skipLibCheck --declaration false --outDir src src/env.ts",
60
+ "lint": "tsc --noEmit"
61
+ },
56
62
  "dependencies": {
57
63
  "@aws-sdk/client-cloudfront": "3.1053.0",
58
64
  "@aws-sdk/client-cloudfront-keyvaluestore": "3.1053.0",
@@ -123,10 +129,5 @@
123
129
  "react": "19.2.6",
124
130
  "ts-jest": "29.4.9",
125
131
  "typescript": "5.9.3"
126
- },
127
- "scripts": {
128
- "test": "jest",
129
- "build": "tsc --build",
130
- "lint": "tsc --noEmit"
131
132
  }
132
- }
133
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Filesystem IO for the Authorization Contract — write/load the per-table files.
3
+ *
4
+ * Kept out of authz-contract.ts so the core (introspect/assemble/diff) stays pure and
5
+ * fs-free. The serialized form is deterministic and byte-stable: a re-pull of an
6
+ * unchanged database produces identical files, so a contract change is always a real,
7
+ * reviewable git diff and never reformatting noise.
8
+ */
9
+
10
+ import fs from 'node:fs/promises';
11
+ import path from 'node:path';
12
+ import type { AuthzContract, TableContract, FunctionContract } from './authz-contract.js';
13
+ import { renderContractMarkdown } from './authz-render.js';
14
+
15
+ /** The human-readable review doc written alongside the JSON. */
16
+ export const REVIEW_FILE = 'AUTHORIZATION.md';
17
+
18
+ /** Stable JSON: object keys sorted recursively, 2-space indent, trailing newline. */
19
+ export function stableStringify(value: unknown): string {
20
+ const sortKeys = (v: any): any => {
21
+ if (Array.isArray(v)) return v.map(sortKeys);
22
+ if (v && typeof v === 'object') {
23
+ return Object.fromEntries(Object.keys(v).sort().map((k) => [k, sortKeys(v[k])]));
24
+ }
25
+ return v;
26
+ };
27
+ return JSON.stringify(sortKeys(value), null, 2) + '\n';
28
+ }
29
+
30
+ /** `public.posts` -> `public.posts.contract.json`. */
31
+ export function tableFileName(table: string): string {
32
+ return `${table}.contract.json`;
33
+ }
34
+
35
+ export const FUNCTIONS_FILE = '_functions.contract.json';
36
+
37
+ /**
38
+ * Write the contract as one file per table plus `_functions.contract.json`. Prunes any
39
+ * stale `*.contract.json` for a table no longer in the contract, so a dropped table
40
+ * leaves no orphan file. The directory is created if absent.
41
+ */
42
+ export async function writeContract(dir: string, contract: AuthzContract): Promise<string[]> {
43
+ await fs.mkdir(dir, { recursive: true });
44
+
45
+ const written = new Set<string>();
46
+ for (const table of contract.tables) {
47
+ const file = tableFileName(table.table);
48
+ await fs.writeFile(path.join(dir, file), stableStringify(table), 'utf8');
49
+ written.add(file);
50
+ }
51
+ await fs.writeFile(path.join(dir, FUNCTIONS_FILE), stableStringify({ functions: contract.functions }), 'utf8');
52
+ written.add(FUNCTIONS_FILE);
53
+
54
+ for (const existing of await listContractFiles(dir)) {
55
+ if (!written.has(existing)) await fs.rm(path.join(dir, existing));
56
+ }
57
+
58
+ // The human-readable review doc, regenerated from the same contract so it can't drift.
59
+ await fs.writeFile(path.join(dir, REVIEW_FILE), renderContractMarkdown(contract), 'utf8');
60
+ written.add(REVIEW_FILE);
61
+ return [...written].sort();
62
+ }
63
+
64
+ /** Load the per-table files back into a contract. Missing dir -> empty contract. */
65
+ export async function loadContract(dir: string): Promise<AuthzContract> {
66
+ let files: string[];
67
+ try {
68
+ files = await listContractFiles(dir);
69
+ } catch {
70
+ return { tables: [], functions: [] };
71
+ }
72
+
73
+ const tables: TableContract[] = [];
74
+ let functions: FunctionContract[] = [];
75
+ for (const file of files) {
76
+ const raw = await fs.readFile(path.join(dir, file), 'utf8');
77
+ const parsed = JSON.parse(raw);
78
+ if (file === FUNCTIONS_FILE) {
79
+ functions = parsed.functions ?? [];
80
+ } else {
81
+ tables.push(parsed as TableContract);
82
+ }
83
+ }
84
+ tables.sort((a, b) => a.table.localeCompare(b.table));
85
+ functions.sort((a, b) => a.name.localeCompare(b.name));
86
+ return { tables, functions };
87
+ }
88
+
89
+ async function listContractFiles(dir: string): Promise<string[]> {
90
+ const entries = await fs.readdir(dir);
91
+ return entries.filter((f) => f.endsWith('.contract.json'));
92
+ }