deepline 0.3.90 → 0.3.92

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 (87) hide show
  1. package/dist/bundling-sources/sdk/src/client.ts +101 -6
  2. package/dist/bundling-sources/sdk/src/monitor-fleet-contract.ts +2 -556
  3. package/dist/bundling-sources/sdk/src/monitor-fleets.ts +2 -2
  4. package/dist/bundling-sources/sdk/src/release.ts +7 -12
  5. package/dist/bundling-sources/sdk/src/types.ts +6 -1
  6. package/dist/bundling-sources/shared_libs/integration-contracts/monitor-fleet-contract.ts +549 -0
  7. package/dist/bundling-sources/shared_libs/monitors/fleets/errors.ts +96 -0
  8. package/dist/bundling-sources/shared_libs/monitors/fleets/limits.ts +8 -0
  9. package/dist/bundling-sources/shared_libs/monitors/fleets/plan.ts +320 -0
  10. package/dist/bundling-sources/shared_libs/monitors/fleets/verdict.ts +106 -0
  11. package/dist/bundling-sources/shared_libs/monitors/http-error-response.ts +5 -1
  12. package/dist/bundling-sources/shared_libs/monitors/monitor-fleet-limits.ts +2 -12
  13. package/dist/bundling-sources/shared_libs/monitors/package.json +107 -5
  14. package/dist/bundling-sources/shared_libs/monitors/provider-monitor-batch-operation.ts +12 -7
  15. package/dist/bundling-sources/shared_libs/observability/scheduled-jobs.json +48 -0
  16. package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +1 -1
  17. package/dist/bundling-sources/shared_libs/play-runtime/db-session-lease-store.ts +578 -0
  18. package/dist/bundling-sources/shared_libs/play-runtime/deployment/commands/roll-execution-relay.ts +521 -0
  19. package/dist/bundling-sources/shared_libs/play-runtime/deployment/commands/roll-receipt-gateway.ts +1378 -0
  20. package/dist/bundling-sources/shared_libs/play-runtime/deployment/commands/runtime-topology.ts +1024 -0
  21. package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-gateway-route-probe.ts +193 -0
  22. package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-machine-lifecycle.ts +104 -0
  23. package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-machines.ts +1844 -0
  24. package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-registry.ts +21 -0
  25. package/dist/bundling-sources/shared_libs/play-runtime/deployment/process-tree.ts +40 -0
  26. package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-gateway-drain.ts +262 -0
  27. package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-surface.ts +98 -0
  28. package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-topology.ts +303 -0
  29. package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-tuple-publication-guardian.ts +70 -0
  30. package/dist/bundling-sources/shared_libs/play-runtime/deployment/runtime-tuple-recovery.ts +519 -0
  31. package/dist/bundling-sources/shared_libs/play-runtime/deployment/vercel-fly-rollback-alignment.ts +280 -0
  32. package/dist/bundling-sources/shared_libs/play-runtime/deployment/vercel-production-gate.ts +159 -0
  33. package/dist/bundling-sources/shared_libs/play-runtime/integration-operation-reader-fence/memory-store.ts +114 -0
  34. package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +44 -5
  35. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/child-play-resolver.ts +26 -0
  36. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +3662 -0
  37. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/progress-sidecar.ts +47 -0
  38. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-deadline.ts +84 -0
  39. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-gateway-transport.ts +91 -0
  40. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-heartbeat.ts +424 -0
  41. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-progress-push.ts +321 -0
  42. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-sheet-persistence-retry.ts +110 -0
  43. package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-terminal-push.ts +625 -0
  44. package/dist/bundling-sources/shared_libs/play-runtime/runtime-api.ts +46 -3
  45. package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-gateway.ts +1 -1
  46. package/dist/bundling-sources/shared_libs/play-runtime/webhook-receipt-retry.ts +50 -0
  47. package/dist/bundling-sources/shared_libs/plays/bundling/repository-adapter.ts +24 -0
  48. package/dist/bundling-sources/shared_libs/plays/child-run-projection.ts +174 -0
  49. package/dist/bundling-sources/shared_libs/plays/deploy/api-client.ts +267 -0
  50. package/dist/bundling-sources/shared_libs/plays/deploy/control-plane-types.ts +73 -0
  51. package/dist/bundling-sources/shared_libs/plays/deploy/manifest.ts +162 -0
  52. package/dist/bundling-sources/shared_libs/plays/deploy/project-serializer.ts +87 -0
  53. package/dist/bundling-sources/shared_libs/plays/deploy/source-file-path.ts +26 -0
  54. package/dist/bundling-sources/shared_libs/plays/deploy/target-paths.ts +36 -0
  55. package/dist/bundling-sources/shared_libs/plays/launch/input-contract.ts +400 -0
  56. package/dist/bundling-sources/shared_libs/plays/launch/prepare.ts +96 -0
  57. package/dist/bundling-sources/shared_libs/plays/local-file-discovery.ts +385 -0
  58. package/dist/bundling-sources/shared_libs/plays/play-grep-search.ts +79 -6
  59. package/dist/bundling-sources/shared_libs/plays/play-reference.ts +117 -0
  60. package/dist/bundling-sources/shared_libs/plays/run-input-summary.ts +260 -0
  61. package/dist/bundling-sources/shared_libs/plays/run-summary-types.ts +49 -0
  62. package/dist/bundling-sources/shared_libs/plays/secrets/errors.ts +19 -0
  63. package/dist/bundling-sources/shared_libs/plays/sheet-filter.ts +347 -0
  64. package/dist/bundling-sources/shared_libs/plays/standard-webhooks.ts +117 -0
  65. package/dist/bundling-sources/shared_libs/plays/status-result.ts +388 -0
  66. package/dist/bundling-sources/shared_libs/plays/transform-factor-structure.ts +377 -0
  67. package/dist/bundling-sources/shared_libs/plays/webhook-payload.ts +113 -0
  68. package/dist/cli/index.js +369 -162
  69. package/dist/cli/index.mjs +369 -162
  70. package/dist/{compiler-manifest-BaJfE-5H.d.mts → compiler-manifest-x14KL83E.d.mts} +1 -1
  71. package/dist/{compiler-manifest-BaJfE-5H.d.ts → compiler-manifest-x14KL83E.d.ts} +1 -1
  72. package/dist/index.d.mts +45 -8
  73. package/dist/index.d.ts +45 -8
  74. package/dist/index.js +61 -14
  75. package/dist/index.mjs +61 -14
  76. package/dist/install-integrity.json +54 -2
  77. package/dist/monitor-fleet-contract.d.mts +3 -10
  78. package/dist/monitor-fleet-contract.d.ts +3 -10
  79. package/dist/monitor-fleet-contract.js +2 -0
  80. package/dist/monitor-fleet-contract.mjs +1 -1
  81. package/dist/plays/bundle-play-file.d.mts +2 -2
  82. package/dist/plays/bundle-play-file.d.ts +2 -2
  83. package/dist/release.d.mts +3 -3
  84. package/dist/release.d.ts +3 -3
  85. package/dist/release.js +7 -12
  86. package/dist/release.mjs +7 -12
  87. package/package.json +2 -2
@@ -0,0 +1,377 @@
1
+ /**
2
+ * Factor structure of a code transform — the descriptive shape a trace UI needs
3
+ * to render an authored lambda as readable factors instead of raw code.
4
+ *
5
+ * A cell produced by a provider call explains itself: there is a tool, an
6
+ * input, a response. A cell produced by CODE does not. The trace can show the
7
+ * lambda verbatim, and most readers cannot read it. This module's types
8
+ * describe the one thing that IS mechanically recoverable from the source: the
9
+ * named intermediate steps, what each one reads, and how they combine.
10
+ *
11
+ * Three properties are load-bearing:
12
+ *
13
+ * 1. **Static, never evaluated.** Nothing here says which branch fired. The
14
+ * structure states "40 when employees >= 1000, else 25 when >= 200, else
15
+ * 10"; the UI juxtaposes the row's actual values beside it so a human draws
16
+ * the conclusion. An extractor that evaluated would be a second, silently
17
+ * divergent implementation of the runtime.
18
+ * 2. **Whole or nothing.** A lambda outside the supported shape yields no
19
+ * structure at all, with a reason. A structure that quietly dropped a
20
+ * statement would read as complete and be a lie, which is worse for a
21
+ * provenance surface than showing the code.
22
+ * 3. **Partial only when marked.** Where a sub-expression cannot be structured,
23
+ * it is carried verbatim and flagged (`opaque`), so the renderer shows the
24
+ * author's own text rather than a paraphrase it cannot justify.
25
+ *
26
+ * These types are deliberately dependency-free: the extractor (which needs the
27
+ * TypeScript compiler) and the renderer model (which runs in the browser) both
28
+ * import them, and only the extractor may pay for the compiler.
29
+ */
30
+
31
+ /** A column read off the transform's row parameter. */
32
+ export type TransformInputRef = {
33
+ /** Dataset column, e.g. `company_profile` for `row.company_profile`. */
34
+ column: string;
35
+ /** Property path under the column, e.g. `['basic_info','name']`. */
36
+ path?: string[];
37
+ };
38
+
39
+ /**
40
+ * Everything an expression reaches for.
41
+ *
42
+ * `externals` are identifiers resolved outside the lambda that are not calls —
43
+ * a module constant, say. They are named rather than inlined: the extractor
44
+ * reads one lambda, and claiming to know an external's value would be a guess.
45
+ */
46
+ export type TransformRefs = {
47
+ inputs: TransformInputRef[];
48
+ factors: string[];
49
+ /** Called functions, by written name (`employeeFloor`, `Number`, `Math.max`). */
50
+ calls: string[];
51
+ externals: string[];
52
+ };
53
+
54
+ /** A value position: a leaf of the structure. `text` is always the authored text. */
55
+ export type TransformValue =
56
+ | { kind: 'literal'; text: string }
57
+ | { kind: 'input'; text: string; input: TransformInputRef }
58
+ | { kind: 'factor'; text: string; name: string }
59
+ | {
60
+ kind: 'call';
61
+ text: string;
62
+ /**
63
+ * The name at the call position. WITHOUT `receiver` it is a function a
64
+ * reader can look up (`employeeFloor`, `Number`, `Math.max`). WITH
65
+ * `receiver` it is a METHOD name and means nothing on its own: `trim` is
66
+ * not a function anybody can find, it is what was done to the receiver.
67
+ */
68
+ callee: string;
69
+ args: TransformValue[];
70
+ /**
71
+ * The value the method was reached THROUGH, when the call is a method
72
+ * call — `String(row.domain)` in `String(row.domain).trim()`.
73
+ *
74
+ * Absent for a plain named call, and the absence is load-bearing: a
75
+ * consumer that evaluates the language's own coercions (`Number`,
76
+ * `String`, `Boolean`) must not fire on `x.Number(y)`, which is somebody
77
+ * else's method that happens to share a name.
78
+ */
79
+ receiver?: TransformValue;
80
+ }
81
+ /**
82
+ * A property reached through another value — `profile.company.name`,
83
+ * `parts[0]`, `String(row.domain).length`.
84
+ *
85
+ * Only a STATIC key: a name, or a literal index. `row[key]` names a property
86
+ * nobody can read off the source, and it stays verbatim rather than being
87
+ * described as a property called `key`.
88
+ *
89
+ * `optional` is the author's `?.`, and it is carried because it changes what
90
+ * happens on a missing object: with it the read yields undefined, without it
91
+ * the run threw. A consumer that flattened the two would state a value for a
92
+ * row whose run never got this far.
93
+ */
94
+ | {
95
+ kind: 'member';
96
+ text: string;
97
+ object: TransformValue;
98
+ property: string;
99
+ optional: boolean;
100
+ }
101
+ /**
102
+ * A fallback chain — `row.name ?? row.legal_name ?? null`.
103
+ *
104
+ * The operands are in the order the author wrote them, which is the order
105
+ * they are tried in. Nothing here says which one won: that depends on the
106
+ * row, and deciding it is the consumer's job over recorded values, exactly as
107
+ * it is for a conditional's arms.
108
+ */
109
+ | {
110
+ kind: 'coalesce';
111
+ operator: TransformCoalesceOperator;
112
+ text: string;
113
+ operands: TransformValue[];
114
+ }
115
+ /**
116
+ * A template literal, as the described parts it joins —
117
+ * `` `${row.first_name} ${row.last_name}` `` is three parts, the middle one a
118
+ * literal space.
119
+ *
120
+ * Only an UNTAGGED template. A tagged one runs the tag's body over the parts,
121
+ * and that body is not in this structure, so it stays verbatim.
122
+ */
123
+ | { kind: 'concat'; text: string; parts: TransformValue[] }
124
+ /**
125
+ * An identifier bound outside the transform — a module constant, an import.
126
+ *
127
+ * NAMED, never valued. The extractor reads one lambda; what a module constant
128
+ * holds at run time is not in it, and a leaf that claimed to know would be
129
+ * guessing. Distinct from `expression` because there IS something to say
130
+ * here: a reader can go and look this name up.
131
+ */
132
+ | { kind: 'external'; text: string; name: string }
133
+ /** Anything else, carried verbatim. Never paraphrased. */
134
+ | { kind: 'expression'; text: string };
135
+
136
+ export type TransformComparisonOperator =
137
+ | '==='
138
+ | '!=='
139
+ | '=='
140
+ | '!='
141
+ | '>='
142
+ | '>'
143
+ | '<='
144
+ | '<';
145
+
146
+ /**
147
+ * A test in a conditional.
148
+ *
149
+ * `truthiness` is the bare `row.headquarters ? 15 : 0` shape — the author asked
150
+ * whether a value is there at all, and the renderer says exactly that rather
151
+ * than inventing a comparison the code does not make.
152
+ */
153
+ export type TransformCondition =
154
+ | {
155
+ kind: 'comparison';
156
+ text: string;
157
+ left: TransformValue;
158
+ operator: TransformComparisonOperator;
159
+ right: TransformValue;
160
+ }
161
+ | {
162
+ kind: 'truthiness';
163
+ text: string;
164
+ subject: TransformValue;
165
+ negated: boolean;
166
+ }
167
+ | { kind: 'all'; text: string; parts: TransformCondition[] }
168
+ | { kind: 'any'; text: string; parts: TransformCondition[] }
169
+ /** Structurally unreadable, carried verbatim. */
170
+ | { kind: 'opaque'; text: string };
171
+
172
+ /** One arm of a ternary chain: this condition selects this value. */
173
+ export type TransformConditionArm = {
174
+ condition: TransformCondition;
175
+ value: TransformValue;
176
+ /** What this arm's test and value read. */
177
+ refs: TransformRefs;
178
+ };
179
+
180
+ /** Arithmetic operators the extractor will flatten into an operand list. */
181
+ export type TransformCombinationOperator = '+' | '-' | '*' | '/' | '%';
182
+
183
+ /**
184
+ * The two fallback operators, kept APART because they ask different questions.
185
+ *
186
+ * `??` takes the first operand that is neither null nor undefined. `||` takes
187
+ * the first TRUTHY one, so it also steps over `0`, `''` and `false` — values an
188
+ * author may well have meant to keep. Describing both as "first present of"
189
+ * would state the wrong rule for half the corpus, so the operator travels with
190
+ * the structure and every renderer says which one it is looking at.
191
+ */
192
+ export type TransformCoalesceOperator = '??' | '||';
193
+
194
+ /**
195
+ * How a factor (or the transform's result) gets its value.
196
+ *
197
+ * `value` with an `expression` leaf is the marked-partial case: the rule was
198
+ * not structurable, and the factor carries `opaque: true` so the renderer
199
+ * prints the author's text instead of a description of it.
200
+ */
201
+ export type TransformRule =
202
+ | { kind: 'value'; value: TransformValue }
203
+ | {
204
+ kind: 'conditional';
205
+ arms: TransformConditionArm[];
206
+ otherwise: TransformValue;
207
+ }
208
+ | {
209
+ kind: 'combination';
210
+ operator: TransformCombinationOperator;
211
+ operands: TransformValue[];
212
+ };
213
+
214
+ /** One named intermediate step: a `const` in the transform body. */
215
+ export type TransformFactor = {
216
+ name: string;
217
+ /** The authored initializer, whitespace-normalized. Always renderable. */
218
+ expression: string;
219
+ rule: TransformRule;
220
+ /** Union of everything the initializer reads. */
221
+ refs: TransformRefs;
222
+ /** True when the rule is verbatim text rather than a described structure. */
223
+ opaque?: true;
224
+ };
225
+
226
+ /** The transform's returned value. */
227
+ export type TransformResult = {
228
+ expression: string;
229
+ rule: TransformRule;
230
+ refs: TransformRefs;
231
+ opaque?: true;
232
+ };
233
+
234
+ /**
235
+ * The extracted structure of one column's transform.
236
+ *
237
+ * `inputColumns` is the join key for the UI: the columns this transform reads,
238
+ * in first-appearance order, so factor lines can be sat beside the trace's own
239
+ * input hops without the renderer re-deriving them.
240
+ */
241
+ export type TransformFactorStructure = {
242
+ /** Dataset column this transform produces. */
243
+ column: string;
244
+ /** Dataset the column belongs to, when the extractor could name it. */
245
+ dataset?: string;
246
+ /** The transform's row parameter name, as authored. */
247
+ rowParam: string;
248
+ factors: TransformFactor[];
249
+ result: TransformResult;
250
+ inputColumns: string[];
251
+ /** Functions called by the transform, by written name. */
252
+ helpers: string[];
253
+ };
254
+
255
+ /**
256
+ * A structure, addressed the way a trace addresses cells.
257
+ *
258
+ * The structure alone says what a transform IS; it does not say which cell it
259
+ * explains. A consumer holding a chain of hops needs both, so the address
260
+ * travels with it — table and column, verbatim as the trace reported them, so
261
+ * the join is a string match and never a guess about namespacing.
262
+ */
263
+ export type TransformFactorEntry = {
264
+ table: string;
265
+ column: string;
266
+ structure: TransformFactorStructure;
267
+ };
268
+
269
+ /**
270
+ * Why no structure was produced.
271
+ *
272
+ * Each value names a shape the extractor refuses to describe rather than
273
+ * describe badly. `unsupported_statement` covers loops, branches, mutation and
274
+ * try/catch in one: the supported body is a run of `const` declarations
275
+ * followed by a `return`, and anything else in it means the description would
276
+ * omit real behavior.
277
+ *
278
+ * The last two are about the extractor rather than the code.
279
+ * `expression_too_deep` is a nesting budget: a described value that nests past
280
+ * it is refused instead of risking the stack. `extraction_failed` is the net
281
+ * under everything — any error the extractor did not anticipate arrives here
282
+ * with its message, so a surface reports "cannot read this" instead of failing
283
+ * the request. Either one appearing on an authored play is a bug worth chasing.
284
+ */
285
+ export type TransformUnstructurableReason =
286
+ | 'column_not_found'
287
+ | 'ambiguous_column'
288
+ | 'not_a_function'
289
+ /**
290
+ * The column is filled by a `steps()` program — a provider cascade — and not
291
+ * by code. There are no factors to describe because there is no arithmetic:
292
+ * the trace explains such a cell with its hops, which is a better answer than
293
+ * anything this module could say about it.
294
+ */
295
+ | 'step_program_column'
296
+ | 'async_transform'
297
+ | 'destructured_row_param'
298
+ | 'row_context_used'
299
+ | 'row_index_used'
300
+ /**
301
+ * The transform read `previousCell` — the value this cell held before the
302
+ * rerun — which the object column form hands its `run` alongside the row.
303
+ *
304
+ * Not a column, and named separately from the row context and the row index
305
+ * for the same reason those two are named separately from each other: a
306
+ * refusal that misnames what it refused sends the next reader looking in the
307
+ * wrong place.
308
+ */
309
+ | 'previous_cell_used'
310
+ /**
311
+ * An object column's `run` was handed its input WHOLE — `(input) =>
312
+ * input.row.domain` rather than `({ row }) => row.domain`.
313
+ *
314
+ * The contract passes `run` one argument, `{ row, ctx, index, previousCell }`,
315
+ * so the row is a property of the parameter rather than the parameter. This
316
+ * module names a row PARAMETER and reads properties off it; a row one level
317
+ * further in is a shape it cannot describe, and describing the envelope
318
+ * instead would report `row` and `ctx` as if they were dataset columns.
319
+ */
320
+ | 'run_input_not_destructured'
321
+ | 'nested_function'
322
+ | 'unsupported_statement'
323
+ | 'unsupported_declaration'
324
+ | 'missing_return'
325
+ | 'dynamic_row_access'
326
+ | 'expression_too_deep'
327
+ | 'extraction_failed';
328
+
329
+ /** Extraction outcome. A failure is a fact with a reason, never an exception. */
330
+ export type TransformFactorExtraction =
331
+ | { structure: TransformFactorStructure; reason: null }
332
+ | { structure: null; reason: TransformUnstructurableReason; detail?: string };
333
+
334
+ /** Empty ref set. Callers merge into a copy; this value is never mutated. */
335
+ export function emptyTransformRefs(): TransformRefs {
336
+ return { inputs: [], factors: [], calls: [], externals: [] };
337
+ }
338
+
339
+ /** Merge ref sets, preserving first-appearance order and deduping. */
340
+ export function mergeTransformRefs(
341
+ ...sets: readonly TransformRefs[]
342
+ ): TransformRefs {
343
+ const merged = emptyTransformRefs();
344
+ const seenInputs = new Set<string>();
345
+ const seenFactors = new Set<string>();
346
+ const seenCalls = new Set<string>();
347
+ const seenExternals = new Set<string>();
348
+ for (const set of sets) {
349
+ for (const input of set.inputs) {
350
+ const key = transformInputKey(input);
351
+ if (seenInputs.has(key)) continue;
352
+ seenInputs.add(key);
353
+ merged.inputs.push(input);
354
+ }
355
+ for (const factor of set.factors) {
356
+ if (seenFactors.has(factor)) continue;
357
+ seenFactors.add(factor);
358
+ merged.factors.push(factor);
359
+ }
360
+ for (const call of set.calls) {
361
+ if (seenCalls.has(call)) continue;
362
+ seenCalls.add(call);
363
+ merged.calls.push(call);
364
+ }
365
+ for (const external of set.externals) {
366
+ if (seenExternals.has(external)) continue;
367
+ seenExternals.add(external);
368
+ merged.externals.push(external);
369
+ }
370
+ }
371
+ return merged;
372
+ }
373
+
374
+ /** Stable identity for an input ref, column plus path. */
375
+ export function transformInputKey(input: TransformInputRef): string {
376
+ return [input.column, ...(input.path ?? [])].join('.');
377
+ }
@@ -0,0 +1,113 @@
1
+ const WEBHOOK_KEY_PREFIX = '__deepline_webhook_key_v1_';
2
+ /**
3
+ * Play inputs are objects or arrays. Rather than acknowledge a scalar JSON or
4
+ * non-JSON delivery and silently replace it with `{}`, retain the exact body
5
+ * in a namespaced input envelope. This name is only introduced for payloads
6
+ * which cannot themselves be a Play input, so it cannot collide with a key in
7
+ * a caller-provided object.
8
+ */
9
+ export const WEBHOOK_RAW_INPUT_KEY = '__deepline_webhook_raw_v1';
10
+ const encoder = new TextEncoder();
11
+ const decoder = new TextDecoder('utf-8', { fatal: true });
12
+
13
+ export type PlayWebhookPayload = Record<string, unknown> | unknown[];
14
+
15
+ export type RetainedWebhookPayload = {
16
+ rawBody: string;
17
+ contentType: string | null;
18
+ encoding: 'utf-8';
19
+ /** Present when the body was valid JSON but not an object or array. */
20
+ jsonValue?: string | number | boolean | null;
21
+ };
22
+
23
+ /**
24
+ * Convert an arbitrary HTTP body into a valid Play input without losing it.
25
+ * Object and array JSON remain byte-for-byte equivalent at the JSON value
26
+ * level; scalar JSON and non-JSON use a deliberately explicit envelope.
27
+ */
28
+ export function parsePlayWebhookPayload(input: {
29
+ rawBody: string;
30
+ contentType?: string | null;
31
+ }): PlayWebhookPayload {
32
+ const contentType = input.contentType?.trim() || null;
33
+ try {
34
+ const parsed = JSON.parse(input.rawBody) as unknown;
35
+ if (Array.isArray(parsed)) return parsed;
36
+ if (parsed && typeof parsed === 'object') {
37
+ return parsed as Record<string, unknown>;
38
+ }
39
+ if (
40
+ parsed === null ||
41
+ typeof parsed === 'string' ||
42
+ typeof parsed === 'number' ||
43
+ typeof parsed === 'boolean'
44
+ ) {
45
+ return {
46
+ [WEBHOOK_RAW_INPUT_KEY]: {
47
+ rawBody: input.rawBody,
48
+ contentType,
49
+ encoding: 'utf-8',
50
+ jsonValue: parsed,
51
+ } satisfies RetainedWebhookPayload,
52
+ };
53
+ }
54
+ } catch {
55
+ // The raw body below is the durable, user-visible value for non-JSON.
56
+ }
57
+ return {
58
+ [WEBHOOK_RAW_INPUT_KEY]: {
59
+ rawBody: input.rawBody,
60
+ contentType,
61
+ encoding: 'utf-8',
62
+ } satisfies RetainedWebhookPayload,
63
+ };
64
+ }
65
+
66
+ function encodeKey(key: string): string {
67
+ return (
68
+ WEBHOOK_KEY_PREFIX +
69
+ Array.from(encoder.encode(key), (byte) =>
70
+ byte.toString(16).padStart(2, '0'),
71
+ ).join('')
72
+ );
73
+ }
74
+
75
+ function decodeKey(key: string): string | null {
76
+ if (!key.startsWith(WEBHOOK_KEY_PREFIX)) return null;
77
+ const encoded = key.slice(WEBHOOK_KEY_PREFIX.length);
78
+ if (!encoded || encoded.length % 2 !== 0 || !/^[0-9a-f]+$/i.test(encoded)) {
79
+ return null;
80
+ }
81
+ try {
82
+ const bytes = new Uint8Array(
83
+ encoded.match(/.{2}/g)!.map((pair) => Number.parseInt(pair, 16)),
84
+ );
85
+ const decoded = decoder.decode(bytes);
86
+ return encodeKey(decoded) === key ? decoded : null;
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
91
+
92
+ function mapKeys(value: unknown, mapKey: (key: string) => string): unknown {
93
+ if (Array.isArray(value)) return value.map((item) => mapKeys(item, mapKey));
94
+ if (!value || typeof value !== 'object') return value;
95
+ return Object.fromEntries(
96
+ Object.entries(value as Record<string, unknown>).map(
97
+ ([key, nestedValue]) => [mapKey(key), mapKeys(nestedValue, mapKey)],
98
+ ),
99
+ );
100
+ }
101
+
102
+ /**
103
+ * Convex only accepts ASCII object keys. Webhook bodies are external JSON, so
104
+ * encode every key losslessly before crossing its client or persistence boundary.
105
+ */
106
+ export function encodeWebhookPayloadForConvex(value: unknown): unknown {
107
+ return mapKeys(value, encodeKey);
108
+ }
109
+
110
+ /** Restores webhook payload keys before they are made available to a Play. */
111
+ export function decodeWebhookPayloadFromConvex(value: unknown): unknown {
112
+ return mapKeys(value, (key) => decodeKey(key) ?? key);
113
+ }