@striae-org/striae 5.1.1 → 5.2.1

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.
Files changed (30) hide show
  1. package/.env.example +41 -11
  2. package/app/utils/data/permissions.ts +4 -2
  3. package/package.json +5 -5
  4. package/scripts/deploy-config/modules/env-utils.sh +322 -0
  5. package/scripts/deploy-config/modules/keys.sh +404 -0
  6. package/scripts/deploy-config/modules/prompt.sh +372 -0
  7. package/scripts/deploy-config/modules/scaffolding.sh +344 -0
  8. package/scripts/deploy-config/modules/validation.sh +365 -0
  9. package/scripts/deploy-config.sh +47 -1572
  10. package/scripts/deploy-worker-secrets.sh +100 -5
  11. package/worker-configuration.d.ts +6 -3
  12. package/workers/audit-worker/package.json +1 -1
  13. package/workers/audit-worker/src/audit-worker.example.ts +188 -6
  14. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  15. package/workers/data-worker/package.json +1 -1
  16. package/workers/data-worker/src/data-worker.example.ts +344 -32
  17. package/workers/data-worker/wrangler.jsonc.example +1 -1
  18. package/workers/image-worker/package.json +1 -1
  19. package/workers/image-worker/src/image-worker.example.ts +190 -5
  20. package/workers/image-worker/wrangler.jsonc.example +1 -1
  21. package/workers/keys-worker/package.json +1 -1
  22. package/workers/keys-worker/wrangler.jsonc.example +1 -1
  23. package/workers/pdf-worker/package.json +1 -1
  24. package/workers/pdf-worker/src/pdf-worker.example.ts +0 -1
  25. package/workers/pdf-worker/wrangler.jsonc.example +1 -5
  26. package/workers/user-worker/package.json +17 -17
  27. package/workers/user-worker/src/encryption-utils.ts +244 -0
  28. package/workers/user-worker/src/user-worker.example.ts +333 -31
  29. package/workers/user-worker/wrangler.jsonc.example +1 -1
  30. package/wrangler.toml.example +1 -1
@@ -2,7 +2,7 @@
2
2
  "name": "KEYS_WORKER_NAME",
3
3
  "account_id": "ACCOUNT_ID",
4
4
  "main": "src/keys.ts",
5
- "compatibility_date": "2026-03-24",
5
+ "compatibility_date": "2026-03-25",
6
6
  "compatibility_flags": [
7
7
  "nodejs_compat"
8
8
  ],
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "devDependencies": {
12
12
  "@cloudflare/puppeteer": "^1.0.6",
13
- "wrangler": "^4.76.0"
13
+ "wrangler": "^4.77.0"
14
14
  },
15
15
  "overrides": {
16
16
  "undici": "7.24.1",
@@ -1,7 +1,6 @@
1
1
  import type { PDFGenerationData, PDFGenerationRequest, ReportModule, ReportPdfOptions } from './report-types';
2
2
 
3
3
  interface Env {
4
- BROWSER: Fetcher;
5
4
  PDF_WORKER_AUTH: string;
6
5
  ACCOUNT_ID?: string;
7
6
  BROWSER_API_TOKEN?: string;
@@ -2,15 +2,11 @@
2
2
  "name": "PDF_WORKER_NAME",
3
3
  "account_id": "ACCOUNT_ID",
4
4
  "main": "src/pdf-worker.ts",
5
- "compatibility_date": "2026-03-24",
5
+ "compatibility_date": "2026-03-25",
6
6
  "compatibility_flags": [
7
7
  "nodejs_compat"
8
8
  ],
9
9
 
10
- "browser": {
11
- "binding": "BROWSER"
12
- },
13
-
14
10
  "observability": {
15
11
  "enabled": true
16
12
  },
@@ -1,18 +1,18 @@
1
1
  {
2
- "name": "user-worker",
3
- "version": "0.0.0",
4
- "private": true,
5
- "scripts": {
6
- "deploy": "wrangler deploy",
7
- "dev": "wrangler dev",
8
- "start": "wrangler dev"
9
- },
10
- "devDependencies": {
11
- "@cloudflare/puppeteer": "^1.0.6",
12
- "wrangler": "^4.76.0"
13
- },
14
- "overrides": {
15
- "undici": "7.24.1",
16
- "yauzl": "3.2.1"
17
- }
18
- }
2
+ "name": "user-worker",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "deploy": "wrangler deploy",
7
+ "dev": "wrangler dev",
8
+ "start": "wrangler dev"
9
+ },
10
+ "devDependencies": {
11
+ "@cloudflare/puppeteer": "^1.0.6",
12
+ "wrangler": "^4.77.0"
13
+ },
14
+ "overrides": {
15
+ "undici": "7.24.1",
16
+ "yauzl": "3.2.1"
17
+ }
18
+ }
@@ -0,0 +1,244 @@
1
+ export interface UserKvEncryptedRecord {
2
+ algorithm: string;
3
+ encryptionVersion: string;
4
+ keyId: string;
5
+ dataIv: string;
6
+ wrappedKey: string;
7
+ ciphertext: string;
8
+ }
9
+
10
+ const USER_KV_ENCRYPTION_ALGORITHM = 'RSA-OAEP-AES-256-GCM';
11
+ const USER_KV_ENCRYPTION_VERSION = '1.0';
12
+
13
+ function base64UrlDecode(value: string): Uint8Array {
14
+ const normalized = value.replace(/-/g, '+').replace(/_/g, '/');
15
+ const padding = '='.repeat((4 - (normalized.length % 4)) % 4);
16
+ const decoded = atob(normalized + padding);
17
+ const bytes = new Uint8Array(decoded.length);
18
+
19
+ for (let index = 0; index < decoded.length; index += 1) {
20
+ bytes[index] = decoded.charCodeAt(index);
21
+ }
22
+
23
+ return bytes;
24
+ }
25
+
26
+ function base64UrlEncode(value: Uint8Array): string {
27
+ let binary = '';
28
+ const chunkSize = 8192;
29
+
30
+ for (let i = 0; i < value.length; i += chunkSize) {
31
+ const chunk = value.subarray(i, Math.min(i + chunkSize, value.length));
32
+ for (let j = 0; j < chunk.length; j += 1) {
33
+ binary += String.fromCharCode(chunk[j]);
34
+ }
35
+ }
36
+
37
+ return btoa(binary)
38
+ .replace(/\+/g, '-')
39
+ .replace(/\//g, '_')
40
+ .replace(/=+$/g, '');
41
+ }
42
+
43
+ function parseSpkiPublicKey(publicKey: string): ArrayBuffer {
44
+ const normalizedKey = publicKey
45
+ .trim()
46
+ .replace(/^['"]|['"]$/g, '')
47
+ .replace(/\\n/g, '\n');
48
+
49
+ const pemBody = normalizedKey
50
+ .replace('-----BEGIN PUBLIC KEY-----', '')
51
+ .replace('-----END PUBLIC KEY-----', '')
52
+ .replace(/\s+/g, '');
53
+
54
+ if (!pemBody) {
55
+ throw new Error('User KV encryption public key is invalid');
56
+ }
57
+
58
+ const binary = atob(pemBody);
59
+ const bytes = new Uint8Array(binary.length);
60
+
61
+ for (let index = 0; index < binary.length; index += 1) {
62
+ bytes[index] = binary.charCodeAt(index);
63
+ }
64
+
65
+ return bytes.buffer;
66
+ }
67
+
68
+ function parsePkcs8PrivateKey(privateKey: string): ArrayBuffer {
69
+ const normalizedKey = privateKey
70
+ .trim()
71
+ .replace(/^['"]|['"]$/g, '')
72
+ .replace(/\\n/g, '\n');
73
+
74
+ const pemBody = normalizedKey
75
+ .replace('-----BEGIN PRIVATE KEY-----', '')
76
+ .replace('-----END PRIVATE KEY-----', '')
77
+ .replace(/\s+/g, '');
78
+
79
+ if (!pemBody) {
80
+ throw new Error('User KV encryption private key is invalid');
81
+ }
82
+
83
+ const binary = atob(pemBody);
84
+ const bytes = new Uint8Array(binary.length);
85
+
86
+ for (let index = 0; index < binary.length; index += 1) {
87
+ bytes[index] = binary.charCodeAt(index);
88
+ }
89
+
90
+ return bytes.buffer;
91
+ }
92
+
93
+ async function importRsaOaepPrivateKey(privateKeyPem: string): Promise<CryptoKey> {
94
+ return crypto.subtle.importKey(
95
+ 'pkcs8',
96
+ parsePkcs8PrivateKey(privateKeyPem),
97
+ {
98
+ name: 'RSA-OAEP',
99
+ hash: 'SHA-256'
100
+ },
101
+ false,
102
+ ['decrypt']
103
+ );
104
+ }
105
+
106
+ async function importRsaOaepPublicKey(publicKeyPem: string): Promise<CryptoKey> {
107
+ return crypto.subtle.importKey(
108
+ 'spki',
109
+ parseSpkiPublicKey(publicKeyPem),
110
+ {
111
+ name: 'RSA-OAEP',
112
+ hash: 'SHA-256'
113
+ },
114
+ false,
115
+ ['encrypt']
116
+ );
117
+ }
118
+
119
+ async function createAesGcmKey(usages: KeyUsage[]): Promise<CryptoKey> {
120
+ return crypto.subtle.generateKey(
121
+ {
122
+ name: 'AES-GCM',
123
+ length: 256
124
+ },
125
+ true,
126
+ usages
127
+ ) as Promise<CryptoKey>;
128
+ }
129
+
130
+ async function wrapAesKey(aesKey: CryptoKey, publicKeyPem: string): Promise<string> {
131
+ const rsaPublicKey = await importRsaOaepPublicKey(publicKeyPem);
132
+ const rawAesKey = await crypto.subtle.exportKey('raw', aesKey);
133
+ const wrappedKey = await crypto.subtle.encrypt(
134
+ { name: 'RSA-OAEP' },
135
+ rsaPublicKey,
136
+ rawAesKey as BufferSource
137
+ );
138
+
139
+ return base64UrlEncode(new Uint8Array(wrappedKey));
140
+ }
141
+
142
+ async function unwrapAesKey(wrappedKeyBase64: string, privateKeyPem: string): Promise<CryptoKey> {
143
+ const rsaPrivateKey = await importRsaOaepPrivateKey(privateKeyPem);
144
+ const wrappedKeyBytes = base64UrlDecode(wrappedKeyBase64);
145
+
146
+ const rawAesKey = await crypto.subtle.decrypt(
147
+ { name: 'RSA-OAEP' },
148
+ rsaPrivateKey,
149
+ wrappedKeyBytes as BufferSource
150
+ );
151
+
152
+ return crypto.subtle.importKey(
153
+ 'raw',
154
+ rawAesKey,
155
+ { name: 'AES-GCM' },
156
+ false,
157
+ ['decrypt']
158
+ );
159
+ }
160
+
161
+ function isEncryptedRecord(value: unknown): value is UserKvEncryptedRecord {
162
+ const candidate = value as Partial<UserKvEncryptedRecord> | null;
163
+ return Boolean(
164
+ candidate &&
165
+ typeof candidate === 'object' &&
166
+ typeof candidate.algorithm === 'string' &&
167
+ typeof candidate.encryptionVersion === 'string' &&
168
+ typeof candidate.keyId === 'string' &&
169
+ typeof candidate.dataIv === 'string' &&
170
+ typeof candidate.wrappedKey === 'string' &&
171
+ typeof candidate.ciphertext === 'string'
172
+ );
173
+ }
174
+
175
+ export function tryParseEncryptedRecord(serializedValue: string): UserKvEncryptedRecord | null {
176
+ let parsed: unknown;
177
+
178
+ try {
179
+ parsed = JSON.parse(serializedValue) as unknown;
180
+ } catch {
181
+ return null;
182
+ }
183
+
184
+ if (!isEncryptedRecord(parsed)) {
185
+ return null;
186
+ }
187
+
188
+ return parsed;
189
+ }
190
+
191
+ export function validateEncryptedRecord(record: UserKvEncryptedRecord): void {
192
+ if (record.algorithm !== USER_KV_ENCRYPTION_ALGORITHM) {
193
+ throw new Error('Unsupported user KV encryption algorithm');
194
+ }
195
+
196
+ if (record.encryptionVersion !== USER_KV_ENCRYPTION_VERSION) {
197
+ throw new Error('Unsupported user KV encryption version');
198
+ }
199
+ }
200
+
201
+ export async function encryptJsonForUserKv(
202
+ plaintextJson: string,
203
+ publicKeyPem: string,
204
+ keyId: string
205
+ ): Promise<string> {
206
+ const aesKey = await createAesGcmKey(['encrypt', 'decrypt']);
207
+ const wrappedKey = await wrapAesKey(aesKey, publicKeyPem);
208
+ const iv = crypto.getRandomValues(new Uint8Array(12));
209
+
210
+ const plaintextBytes = new TextEncoder().encode(plaintextJson);
211
+ const ciphertextBuffer = await crypto.subtle.encrypt(
212
+ { name: 'AES-GCM', iv: iv as BufferSource },
213
+ aesKey,
214
+ plaintextBytes as BufferSource
215
+ );
216
+
217
+ const encryptedRecord: UserKvEncryptedRecord = {
218
+ algorithm: USER_KV_ENCRYPTION_ALGORITHM,
219
+ encryptionVersion: USER_KV_ENCRYPTION_VERSION,
220
+ keyId,
221
+ dataIv: base64UrlEncode(iv),
222
+ wrappedKey,
223
+ ciphertext: base64UrlEncode(new Uint8Array(ciphertextBuffer))
224
+ };
225
+
226
+ return JSON.stringify(encryptedRecord);
227
+ }
228
+
229
+ export async function decryptJsonFromUserKv(
230
+ record: UserKvEncryptedRecord,
231
+ privateKeyPem: string
232
+ ): Promise<string> {
233
+ const aesKey = await unwrapAesKey(record.wrappedKey, privateKeyPem);
234
+ const iv = base64UrlDecode(record.dataIv);
235
+ const ciphertext = base64UrlDecode(record.ciphertext);
236
+
237
+ const plaintext = await crypto.subtle.decrypt(
238
+ { name: 'AES-GCM', iv: iv as BufferSource },
239
+ aesKey,
240
+ ciphertext as BufferSource
241
+ );
242
+
243
+ return new TextDecoder().decode(plaintext);
244
+ }