deepline 0.1.65 → 0.1.67
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/dist/cli/index.js +385 -2
- package/dist/cli/index.mjs +385 -2
- package/dist/index.d.mts +78 -0
- package/dist/index.d.ts +78 -0
- package/dist/index.js +60 -2
- package/dist/index.mjs +60 -2
- package/dist/repo/apps/play-runner-workers/src/coordinator-entry.ts +475 -69
- package/dist/repo/apps/play-runner-workers/src/dedup-do.ts +134 -0
- package/dist/repo/apps/play-runner-workers/src/entry.ts +37 -49
- package/dist/repo/apps/play-runner-workers/src/runtime/harness-receipt-store.ts +13 -0
- package/dist/repo/apps/play-runner-workers/src/runtime/receipts.ts +10 -136
- package/dist/repo/sdk/src/client.ts +73 -0
- package/dist/repo/sdk/src/http.ts +9 -1
- package/dist/repo/sdk/src/plays/harness-stub.ts +4 -0
- package/dist/repo/sdk/src/release.ts +2 -2
- package/dist/repo/sdk/src/types.ts +59 -0
- package/dist/repo/shared_libs/play-runtime/db-session-crypto.ts +312 -0
- package/dist/repo/shared_libs/play-runtime/db-session-plan.ts +69 -0
- package/dist/repo/shared_libs/play-runtime/db-session.ts +439 -0
- package/dist/repo/shared_libs/play-runtime/work-receipts.ts +92 -0
- package/package.json +1 -1
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
const MAX_DIAGNOSTIC_HEADER_LENGTH = 120;
|
|
36
36
|
|
|
37
37
|
interface RequestOptions {
|
|
38
|
-
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
38
|
+
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
39
39
|
body?: unknown;
|
|
40
40
|
formData?: FormData | (() => FormData);
|
|
41
41
|
headers?: Record<string, string>;
|
|
@@ -392,6 +392,14 @@ export class HttpClient {
|
|
|
392
392
|
});
|
|
393
393
|
}
|
|
394
394
|
|
|
395
|
+
async patch<T = unknown>(
|
|
396
|
+
path: string,
|
|
397
|
+
body: unknown,
|
|
398
|
+
headers?: Record<string, string>,
|
|
399
|
+
): Promise<T> {
|
|
400
|
+
return this.request<T>(path, { method: 'PATCH', body, headers });
|
|
401
|
+
}
|
|
402
|
+
|
|
395
403
|
/**
|
|
396
404
|
* Send a DELETE request.
|
|
397
405
|
*
|
|
@@ -168,6 +168,8 @@ export async function harnessReadSheetDatasetRows(
|
|
|
168
168
|
*/
|
|
169
169
|
export async function harnessPrewarmPostgresSessions(input: {
|
|
170
170
|
executorToken: string;
|
|
171
|
+
orgId: string;
|
|
172
|
+
playName: string;
|
|
171
173
|
sessions: PreloadedRuntimeDbSessionInput[];
|
|
172
174
|
}): Promise<{ ok: true; sessions: number }> {
|
|
173
175
|
return requireBinding().prewarmPostgresSessions(input);
|
|
@@ -180,6 +182,7 @@ export async function harnessPrewarmPostgresSessions(input: {
|
|
|
180
182
|
export async function harnessStartSheetDataset(input: {
|
|
181
183
|
baseUrl: string;
|
|
182
184
|
executorToken: string;
|
|
185
|
+
orgId: string;
|
|
183
186
|
preloadedDbSessions?: PreloadedRuntimeDbSessionInput[] | null;
|
|
184
187
|
playName: string;
|
|
185
188
|
tableNamespace: string;
|
|
@@ -206,6 +209,7 @@ export async function harnessStartSheetDataset(input: {
|
|
|
206
209
|
export async function harnessPersistCompletedSheetRows(input: {
|
|
207
210
|
baseUrl: string;
|
|
208
211
|
executorToken: string;
|
|
212
|
+
orgId: string;
|
|
209
213
|
preloadedDbSessions?: PreloadedRuntimeDbSessionInput[] | null;
|
|
210
214
|
playName: string;
|
|
211
215
|
tableNamespace: string;
|
|
@@ -50,10 +50,10 @@ export type SdkRelease = {
|
|
|
50
50
|
};
|
|
51
51
|
|
|
52
52
|
export const SDK_RELEASE = {
|
|
53
|
-
version: '0.1.
|
|
53
|
+
version: '0.1.67',
|
|
54
54
|
apiContract: '2026-05-play-bootstrap-dataset-summary',
|
|
55
55
|
supportPolicy: {
|
|
56
|
-
latest: '0.1.
|
|
56
|
+
latest: '0.1.67',
|
|
57
57
|
minimumSupported: '0.1.53',
|
|
58
58
|
deprecatedBelow: '0.1.53',
|
|
59
59
|
},
|
|
@@ -889,3 +889,62 @@ export interface DeletePlayResult {
|
|
|
889
889
|
deletedBindingCount: number;
|
|
890
890
|
deletedRunCount: number;
|
|
891
891
|
}
|
|
892
|
+
|
|
893
|
+
// ——————————————————————————————————————————————————————————
|
|
894
|
+
// Shareable play pages
|
|
895
|
+
// ——————————————————————————————————————————————————————————
|
|
896
|
+
|
|
897
|
+
/** Owner-facing view of a play's public share page. */
|
|
898
|
+
export interface SharePageOwnerView {
|
|
899
|
+
shareSlug: string;
|
|
900
|
+
publishedRevisionId: string;
|
|
901
|
+
publishedVersion: number;
|
|
902
|
+
visibility: string;
|
|
903
|
+
seoIndexing: 'index' | 'noindex';
|
|
904
|
+
showAverageDeeplineCost: boolean;
|
|
905
|
+
showAverageLatency: boolean;
|
|
906
|
+
/** Stable public path, e.g. `/p/{shareSlug}`. */
|
|
907
|
+
publicPath: string;
|
|
908
|
+
/** Version-pinned canonical path, e.g. `/p/{shareSlug}/v/{version}`. */
|
|
909
|
+
canonicalPath: string;
|
|
910
|
+
createdAt: number;
|
|
911
|
+
updatedAt: number;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/** One row in the owner-facing revision picker for sharing. */
|
|
915
|
+
export interface SharePageRevisionOption {
|
|
916
|
+
revisionId: string;
|
|
917
|
+
version: number;
|
|
918
|
+
isLive: boolean;
|
|
919
|
+
isWorking: boolean;
|
|
920
|
+
isPublished: boolean;
|
|
921
|
+
hasMap: boolean;
|
|
922
|
+
hasCard: boolean;
|
|
923
|
+
createdAt: number;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/** Status payload from `GET/POST/PATCH /api/v2/plays/:name/share`. */
|
|
927
|
+
export interface SharePageStatus {
|
|
928
|
+
playName: string;
|
|
929
|
+
share: SharePageOwnerView | null;
|
|
930
|
+
publishedCopy: unknown | null;
|
|
931
|
+
revisions: SharePageRevisionOption[];
|
|
932
|
+
/** Present on publish responses when share-card generation was non-strict. */
|
|
933
|
+
warning?: string | null;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
export interface PublishSharePageRequest {
|
|
937
|
+
/** The revision to publish/repoint the public page to. */
|
|
938
|
+
revisionId: string;
|
|
939
|
+
/** Must be true — acknowledges the page is publicly viewable. */
|
|
940
|
+
acknowledgedUnlisted: true;
|
|
941
|
+
showAverageDeeplineCost?: boolean;
|
|
942
|
+
showAverageLatency?: boolean;
|
|
943
|
+
seoIndexing?: 'index' | 'noindex';
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
export interface UpdateSharePageRequest {
|
|
947
|
+
showAverageDeeplineCost?: boolean;
|
|
948
|
+
showAverageLatency?: boolean;
|
|
949
|
+
seoIndexing?: 'index' | 'noindex';
|
|
950
|
+
}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import type { CreateDbSessionResponse } from './db-session';
|
|
2
|
+
|
|
3
|
+
const POSTGRES_URL_ENCRYPTION_ALGORITHM = 'AES-GCM' as const;
|
|
4
|
+
const POSTGRES_URL_ENCRYPTION_KEY_ID =
|
|
5
|
+
'deepline-runtime-db-session-url:v1' as const;
|
|
6
|
+
const POSTGRES_URL_PUBLIC_KEY_ENCRYPTION_KEY_ID =
|
|
7
|
+
'deepline-runtime-db-session-url:v2' as const;
|
|
8
|
+
const POSTGRES_URL_ENCRYPTION_LABEL =
|
|
9
|
+
'deepline:runtime-db-session-postgres-url:v1';
|
|
10
|
+
const POSTGRES_URL_PUBLIC_KEY_ENCRYPTION_ALGORITHM =
|
|
11
|
+
'RSA-OAEP-256+A256GCM' as const;
|
|
12
|
+
const IV_LENGTH_BYTES = 12;
|
|
13
|
+
const AUTH_TAG_LENGTH_BYTES = 16;
|
|
14
|
+
|
|
15
|
+
export type SharedSecretEncryptedPostgresUrl = {
|
|
16
|
+
alg: 'A256GCM';
|
|
17
|
+
kid: typeof POSTGRES_URL_ENCRYPTION_KEY_ID;
|
|
18
|
+
iv: string;
|
|
19
|
+
ciphertext: string;
|
|
20
|
+
tag: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type PublicKeyEncryptedPostgresUrl = {
|
|
24
|
+
alg: typeof POSTGRES_URL_PUBLIC_KEY_ENCRYPTION_ALGORITHM;
|
|
25
|
+
kid: typeof POSTGRES_URL_PUBLIC_KEY_ENCRYPTION_KEY_ID;
|
|
26
|
+
wrappedKey: string;
|
|
27
|
+
iv: string;
|
|
28
|
+
ciphertext: string;
|
|
29
|
+
tag: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type EncryptedPostgresUrl =
|
|
33
|
+
| SharedSecretEncryptedPostgresUrl
|
|
34
|
+
| PublicKeyEncryptedPostgresUrl;
|
|
35
|
+
|
|
36
|
+
export type PostgresUrlEncryptionRequest = {
|
|
37
|
+
alg: typeof POSTGRES_URL_PUBLIC_KEY_ENCRYPTION_ALGORITHM;
|
|
38
|
+
publicKeyJwk: JsonWebKey;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type PostgresUrlDecryptionKey = {
|
|
42
|
+
request: PostgresUrlEncryptionRequest;
|
|
43
|
+
privateKey: CryptoKey;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function encodeBase64Url(bytes: Uint8Array): string {
|
|
47
|
+
const base64 =
|
|
48
|
+
typeof Buffer !== 'undefined'
|
|
49
|
+
? Buffer.from(bytes).toString('base64')
|
|
50
|
+
: btoa(String.fromCharCode(...bytes));
|
|
51
|
+
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function decodeBase64Url(value: string): Uint8Array {
|
|
55
|
+
const padding =
|
|
56
|
+
value.length % 4 === 0 ? '' : '='.repeat(4 - (value.length % 4));
|
|
57
|
+
const normalized = value.replace(/-/g, '+').replace(/_/g, '/') + padding;
|
|
58
|
+
if (typeof Buffer !== 'undefined') {
|
|
59
|
+
return new Uint8Array(Buffer.from(normalized, 'base64'));
|
|
60
|
+
}
|
|
61
|
+
return Uint8Array.from(atob(normalized), (char) => char.charCodeAt(0));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
|
|
65
|
+
return bytes.buffer.slice(
|
|
66
|
+
bytes.byteOffset,
|
|
67
|
+
bytes.byteOffset + bytes.byteLength,
|
|
68
|
+
) as ArrayBuffer;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
type RsaOaepKeyGenAlgorithm = {
|
|
72
|
+
name: 'RSA-OAEP';
|
|
73
|
+
modulusLength: number;
|
|
74
|
+
publicExponent: Uint8Array;
|
|
75
|
+
hash: string;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
type RsaOaepImportAlgorithm = {
|
|
79
|
+
name: 'RSA-OAEP';
|
|
80
|
+
hash: string;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
function rsaOaepAlgorithm(): RsaOaepKeyGenAlgorithm {
|
|
84
|
+
return {
|
|
85
|
+
name: 'RSA-OAEP',
|
|
86
|
+
modulusLength: 2048,
|
|
87
|
+
publicExponent: new Uint8Array([1, 0, 1]),
|
|
88
|
+
hash: 'SHA-256',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function rsaOaepImportAlgorithm(): RsaOaepImportAlgorithm {
|
|
93
|
+
return {
|
|
94
|
+
name: 'RSA-OAEP',
|
|
95
|
+
hash: 'SHA-256',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function derivePostgresUrlEncryptionKey(
|
|
100
|
+
secret: string,
|
|
101
|
+
): Promise<CryptoKey> {
|
|
102
|
+
const normalizedSecret = secret.trim();
|
|
103
|
+
if (!normalizedSecret) {
|
|
104
|
+
throw new Error('Runtime DB session encryption secret is empty.');
|
|
105
|
+
}
|
|
106
|
+
const keyBytes = new Uint8Array(
|
|
107
|
+
await crypto.subtle.digest(
|
|
108
|
+
'SHA-256',
|
|
109
|
+
new TextEncoder().encode(
|
|
110
|
+
`${POSTGRES_URL_ENCRYPTION_LABEL}:${normalizedSecret}`,
|
|
111
|
+
),
|
|
112
|
+
),
|
|
113
|
+
);
|
|
114
|
+
return await crypto.subtle.importKey(
|
|
115
|
+
'raw',
|
|
116
|
+
toArrayBuffer(keyBytes),
|
|
117
|
+
{ name: POSTGRES_URL_ENCRYPTION_ALGORITHM },
|
|
118
|
+
false,
|
|
119
|
+
['encrypt', 'decrypt'],
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function dbSessionPostgresUrlAad(
|
|
124
|
+
session: Omit<
|
|
125
|
+
CreateDbSessionResponse,
|
|
126
|
+
'postgresUrl' | 'encryptedPostgresUrl'
|
|
127
|
+
>,
|
|
128
|
+
): string {
|
|
129
|
+
return JSON.stringify({
|
|
130
|
+
sessionId: session.sessionId,
|
|
131
|
+
expiresAt: session.expiresAt,
|
|
132
|
+
playName: session.playName,
|
|
133
|
+
target: session.target,
|
|
134
|
+
operations: [...session.operations].sort(),
|
|
135
|
+
postgres: session.postgres ?? null,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function generateDbSessionPostgresUrlDecryptionKey(): Promise<PostgresUrlDecryptionKey> {
|
|
140
|
+
const keyPair = (await crypto.subtle.generateKey(rsaOaepAlgorithm(), true, [
|
|
141
|
+
'encrypt',
|
|
142
|
+
'decrypt',
|
|
143
|
+
])) as CryptoKeyPair;
|
|
144
|
+
const publicKeyJwk = (await crypto.subtle.exportKey(
|
|
145
|
+
'jwk',
|
|
146
|
+
keyPair.publicKey,
|
|
147
|
+
)) as JsonWebKey;
|
|
148
|
+
const serializablePublicKeyJwk: JsonWebKey = {
|
|
149
|
+
kty: publicKeyJwk.kty,
|
|
150
|
+
n: publicKeyJwk.n,
|
|
151
|
+
e: publicKeyJwk.e,
|
|
152
|
+
alg: publicKeyJwk.alg,
|
|
153
|
+
ext: publicKeyJwk.ext,
|
|
154
|
+
key_ops: publicKeyJwk.key_ops ? [...publicKeyJwk.key_ops] : undefined,
|
|
155
|
+
};
|
|
156
|
+
return {
|
|
157
|
+
request: {
|
|
158
|
+
alg: POSTGRES_URL_PUBLIC_KEY_ENCRYPTION_ALGORITHM,
|
|
159
|
+
publicKeyJwk: serializablePublicKeyJwk,
|
|
160
|
+
},
|
|
161
|
+
privateKey: keyPair.privateKey,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function encryptDbSessionPostgresUrl(input: {
|
|
166
|
+
postgresUrl: string;
|
|
167
|
+
secret: string;
|
|
168
|
+
aad: string;
|
|
169
|
+
}): Promise<SharedSecretEncryptedPostgresUrl> {
|
|
170
|
+
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH_BYTES));
|
|
171
|
+
const key = await derivePostgresUrlEncryptionKey(input.secret);
|
|
172
|
+
const encrypted = new Uint8Array(
|
|
173
|
+
await crypto.subtle.encrypt(
|
|
174
|
+
{
|
|
175
|
+
name: POSTGRES_URL_ENCRYPTION_ALGORITHM,
|
|
176
|
+
iv: toArrayBuffer(iv),
|
|
177
|
+
additionalData: new TextEncoder().encode(input.aad),
|
|
178
|
+
tagLength: AUTH_TAG_LENGTH_BYTES * 8,
|
|
179
|
+
},
|
|
180
|
+
key,
|
|
181
|
+
new TextEncoder().encode(input.postgresUrl),
|
|
182
|
+
),
|
|
183
|
+
);
|
|
184
|
+
return {
|
|
185
|
+
alg: 'A256GCM',
|
|
186
|
+
kid: POSTGRES_URL_ENCRYPTION_KEY_ID,
|
|
187
|
+
iv: encodeBase64Url(iv),
|
|
188
|
+
ciphertext: encodeBase64Url(encrypted.slice(0, -AUTH_TAG_LENGTH_BYTES)),
|
|
189
|
+
tag: encodeBase64Url(encrypted.slice(-AUTH_TAG_LENGTH_BYTES)),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function encryptDbSessionPostgresUrlWithPublicKey(input: {
|
|
194
|
+
postgresUrl: string;
|
|
195
|
+
request: PostgresUrlEncryptionRequest;
|
|
196
|
+
aad: string;
|
|
197
|
+
}): Promise<PublicKeyEncryptedPostgresUrl> {
|
|
198
|
+
if (input.request.alg !== POSTGRES_URL_PUBLIC_KEY_ENCRYPTION_ALGORITHM) {
|
|
199
|
+
throw new Error('Unsupported runtime DB session public-key envelope.');
|
|
200
|
+
}
|
|
201
|
+
const publicKey = (await crypto.subtle.importKey(
|
|
202
|
+
'jwk',
|
|
203
|
+
input.request.publicKeyJwk,
|
|
204
|
+
rsaOaepImportAlgorithm(),
|
|
205
|
+
false,
|
|
206
|
+
['encrypt'],
|
|
207
|
+
)) as CryptoKey;
|
|
208
|
+
const contentKey = (await crypto.subtle.generateKey(
|
|
209
|
+
{ name: POSTGRES_URL_ENCRYPTION_ALGORITHM, length: 256 },
|
|
210
|
+
true,
|
|
211
|
+
['encrypt', 'decrypt'],
|
|
212
|
+
)) as CryptoKey;
|
|
213
|
+
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH_BYTES));
|
|
214
|
+
const encrypted = new Uint8Array(
|
|
215
|
+
await crypto.subtle.encrypt(
|
|
216
|
+
{
|
|
217
|
+
name: POSTGRES_URL_ENCRYPTION_ALGORITHM,
|
|
218
|
+
iv: toArrayBuffer(iv),
|
|
219
|
+
additionalData: new TextEncoder().encode(input.aad),
|
|
220
|
+
tagLength: AUTH_TAG_LENGTH_BYTES * 8,
|
|
221
|
+
},
|
|
222
|
+
contentKey,
|
|
223
|
+
new TextEncoder().encode(input.postgresUrl),
|
|
224
|
+
),
|
|
225
|
+
);
|
|
226
|
+
const rawContentKey = (await crypto.subtle.exportKey(
|
|
227
|
+
'raw',
|
|
228
|
+
contentKey,
|
|
229
|
+
)) as ArrayBuffer;
|
|
230
|
+
const wrappedKey = new Uint8Array(
|
|
231
|
+
await crypto.subtle.encrypt({ name: 'RSA-OAEP' }, publicKey, rawContentKey),
|
|
232
|
+
);
|
|
233
|
+
return {
|
|
234
|
+
alg: POSTGRES_URL_PUBLIC_KEY_ENCRYPTION_ALGORITHM,
|
|
235
|
+
kid: POSTGRES_URL_PUBLIC_KEY_ENCRYPTION_KEY_ID,
|
|
236
|
+
wrappedKey: encodeBase64Url(wrappedKey),
|
|
237
|
+
iv: encodeBase64Url(iv),
|
|
238
|
+
ciphertext: encodeBase64Url(encrypted.slice(0, -AUTH_TAG_LENGTH_BYTES)),
|
|
239
|
+
tag: encodeBase64Url(encrypted.slice(-AUTH_TAG_LENGTH_BYTES)),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export async function decryptDbSessionPostgresUrl(input: {
|
|
244
|
+
encrypted: EncryptedPostgresUrl;
|
|
245
|
+
secret: string;
|
|
246
|
+
aad: string;
|
|
247
|
+
}): Promise<string> {
|
|
248
|
+
if (
|
|
249
|
+
input.encrypted.alg !== 'A256GCM' ||
|
|
250
|
+
input.encrypted.kid !== POSTGRES_URL_ENCRYPTION_KEY_ID
|
|
251
|
+
) {
|
|
252
|
+
throw new Error('Unsupported runtime DB session URL encryption envelope.');
|
|
253
|
+
}
|
|
254
|
+
const ciphertext = decodeBase64Url(input.encrypted.ciphertext);
|
|
255
|
+
const tag = decodeBase64Url(input.encrypted.tag);
|
|
256
|
+
const combined = new Uint8Array(ciphertext.byteLength + tag.byteLength);
|
|
257
|
+
combined.set(ciphertext, 0);
|
|
258
|
+
combined.set(tag, ciphertext.byteLength);
|
|
259
|
+
const key = await derivePostgresUrlEncryptionKey(input.secret);
|
|
260
|
+
const plaintext = await crypto.subtle.decrypt(
|
|
261
|
+
{
|
|
262
|
+
name: POSTGRES_URL_ENCRYPTION_ALGORITHM,
|
|
263
|
+
iv: toArrayBuffer(decodeBase64Url(input.encrypted.iv)),
|
|
264
|
+
additionalData: new TextEncoder().encode(input.aad),
|
|
265
|
+
tagLength: AUTH_TAG_LENGTH_BYTES * 8,
|
|
266
|
+
},
|
|
267
|
+
key,
|
|
268
|
+
toArrayBuffer(combined),
|
|
269
|
+
);
|
|
270
|
+
return new TextDecoder().decode(plaintext);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export async function decryptDbSessionPostgresUrlWithPrivateKey(input: {
|
|
274
|
+
encrypted: EncryptedPostgresUrl;
|
|
275
|
+
privateKey: CryptoKey;
|
|
276
|
+
aad: string;
|
|
277
|
+
}): Promise<string> {
|
|
278
|
+
if (
|
|
279
|
+
input.encrypted.alg !== POSTGRES_URL_PUBLIC_KEY_ENCRYPTION_ALGORITHM ||
|
|
280
|
+
input.encrypted.kid !== POSTGRES_URL_PUBLIC_KEY_ENCRYPTION_KEY_ID
|
|
281
|
+
) {
|
|
282
|
+
throw new Error('Unsupported runtime DB session URL public-key envelope.');
|
|
283
|
+
}
|
|
284
|
+
const rawContentKey = await crypto.subtle.decrypt(
|
|
285
|
+
{ name: 'RSA-OAEP' },
|
|
286
|
+
input.privateKey,
|
|
287
|
+
toArrayBuffer(decodeBase64Url(input.encrypted.wrappedKey)),
|
|
288
|
+
);
|
|
289
|
+
const contentKey = await crypto.subtle.importKey(
|
|
290
|
+
'raw',
|
|
291
|
+
rawContentKey,
|
|
292
|
+
{ name: POSTGRES_URL_ENCRYPTION_ALGORITHM },
|
|
293
|
+
false,
|
|
294
|
+
['decrypt'],
|
|
295
|
+
);
|
|
296
|
+
const ciphertext = decodeBase64Url(input.encrypted.ciphertext);
|
|
297
|
+
const tag = decodeBase64Url(input.encrypted.tag);
|
|
298
|
+
const combined = new Uint8Array(ciphertext.byteLength + tag.byteLength);
|
|
299
|
+
combined.set(ciphertext, 0);
|
|
300
|
+
combined.set(tag, ciphertext.byteLength);
|
|
301
|
+
const plaintext = await crypto.subtle.decrypt(
|
|
302
|
+
{
|
|
303
|
+
name: POSTGRES_URL_ENCRYPTION_ALGORITHM,
|
|
304
|
+
iv: toArrayBuffer(decodeBase64Url(input.encrypted.iv)),
|
|
305
|
+
additionalData: new TextEncoder().encode(input.aad),
|
|
306
|
+
tagLength: AUTH_TAG_LENGTH_BYTES * 8,
|
|
307
|
+
},
|
|
308
|
+
contentKey,
|
|
309
|
+
toArrayBuffer(combined),
|
|
310
|
+
);
|
|
311
|
+
return new TextDecoder().decode(plaintext);
|
|
312
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import {
|
|
2
|
+
flattenStaticPipeline,
|
|
3
|
+
resolveSheetContractForTableNamespace,
|
|
4
|
+
type PlaySheetContract,
|
|
5
|
+
type PlayStaticPipeline,
|
|
6
|
+
} from '../plays/static-pipeline';
|
|
7
|
+
import {
|
|
8
|
+
RUNTIME_SHEET_ROWS_LOGICAL_TABLE,
|
|
9
|
+
RUNTIME_WORK_RECEIPT_LOGICAL_TABLE,
|
|
10
|
+
RUNTIME_WORK_RECEIPT_TABLE_NAMESPACE,
|
|
11
|
+
type DbLogicalTable,
|
|
12
|
+
type DbSessionLimits,
|
|
13
|
+
type DbSessionOperation,
|
|
14
|
+
} from './db-session';
|
|
15
|
+
|
|
16
|
+
export type RuntimeDbSessionRequirement = {
|
|
17
|
+
tableNamespace: string;
|
|
18
|
+
logicalTable: DbLogicalTable;
|
|
19
|
+
operations: DbSessionOperation[];
|
|
20
|
+
limits?: DbSessionLimits;
|
|
21
|
+
sheetContract?: PlaySheetContract | null;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function planRuntimeSheetDbSessionRequirements(
|
|
25
|
+
pipeline: PlayStaticPipeline | null,
|
|
26
|
+
): RuntimeDbSessionRequirement[] {
|
|
27
|
+
if (!pipeline) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
const byNamespace = new Map<string, PlaySheetContract>();
|
|
31
|
+
for (const substep of flattenStaticPipeline(pipeline)) {
|
|
32
|
+
if (substep.type !== 'map') continue;
|
|
33
|
+
const tableNamespace = (
|
|
34
|
+
substep.tableNamespace ??
|
|
35
|
+
substep.field ??
|
|
36
|
+
''
|
|
37
|
+
).trim();
|
|
38
|
+
if (!tableNamespace) continue;
|
|
39
|
+
const sheetContract =
|
|
40
|
+
resolveSheetContractForTableNamespace(pipeline, tableNamespace) ??
|
|
41
|
+
substep.sheetContract ??
|
|
42
|
+
null;
|
|
43
|
+
if (!sheetContract) continue;
|
|
44
|
+
byNamespace.set(tableNamespace, sheetContract);
|
|
45
|
+
}
|
|
46
|
+
return [...byNamespace.entries()].map(([tableNamespace, sheetContract]) => ({
|
|
47
|
+
tableNamespace,
|
|
48
|
+
logicalTable: RUNTIME_SHEET_ROWS_LOGICAL_TABLE,
|
|
49
|
+
operations: ['rows.read', 'rows.upsert'],
|
|
50
|
+
sheetContract,
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function runtimeWorkReceiptDbSessionRequirement(): RuntimeDbSessionRequirement {
|
|
55
|
+
return {
|
|
56
|
+
tableNamespace: RUNTIME_WORK_RECEIPT_TABLE_NAMESPACE,
|
|
57
|
+
logicalTable: RUNTIME_WORK_RECEIPT_LOGICAL_TABLE,
|
|
58
|
+
operations: ['rows.read', 'rows.upsert'],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function planRuntimeDbSessionRequirements(
|
|
63
|
+
pipeline: PlayStaticPipeline | null,
|
|
64
|
+
): RuntimeDbSessionRequirement[] {
|
|
65
|
+
return [
|
|
66
|
+
...planRuntimeSheetDbSessionRequirements(pipeline),
|
|
67
|
+
runtimeWorkReceiptDbSessionRequirement(),
|
|
68
|
+
];
|
|
69
|
+
}
|