@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.
- package/.env.example +41 -11
- package/app/utils/data/permissions.ts +4 -2
- package/package.json +5 -5
- package/scripts/deploy-config/modules/env-utils.sh +322 -0
- package/scripts/deploy-config/modules/keys.sh +404 -0
- package/scripts/deploy-config/modules/prompt.sh +372 -0
- package/scripts/deploy-config/modules/scaffolding.sh +344 -0
- package/scripts/deploy-config/modules/validation.sh +365 -0
- package/scripts/deploy-config.sh +47 -1572
- package/scripts/deploy-worker-secrets.sh +100 -5
- package/worker-configuration.d.ts +6 -3
- package/workers/audit-worker/package.json +1 -1
- package/workers/audit-worker/src/audit-worker.example.ts +188 -6
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/package.json +1 -1
- package/workers/data-worker/src/data-worker.example.ts +344 -32
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/package.json +1 -1
- package/workers/image-worker/src/image-worker.example.ts +190 -5
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/keys-worker/package.json +1 -1
- package/workers/keys-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/package.json +1 -1
- package/workers/pdf-worker/src/pdf-worker.example.ts +0 -1
- package/workers/pdf-worker/wrangler.jsonc.example +1 -5
- package/workers/user-worker/package.json +17 -17
- package/workers/user-worker/src/encryption-utils.ts +244 -0
- package/workers/user-worker/src/user-worker.example.ts +333 -31
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- package/wrangler.toml.example +1 -1
|
@@ -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-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
+
}
|