@opensaas/stack-core 0.23.0 → 0.25.0

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 (77) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +256 -0
  3. package/dist/access/access-filter.d.ts +39 -0
  4. package/dist/access/access-filter.d.ts.map +1 -1
  5. package/dist/access/access-filter.js +121 -0
  6. package/dist/access/access-filter.js.map +1 -1
  7. package/dist/access/field-access.d.ts +1 -0
  8. package/dist/access/field-access.d.ts.map +1 -1
  9. package/dist/access/field-access.js +79 -4
  10. package/dist/access/field-access.js.map +1 -1
  11. package/dist/access/field-access.test.js +213 -0
  12. package/dist/access/field-access.test.js.map +1 -1
  13. package/dist/access/index.d.ts +1 -1
  14. package/dist/access/index.d.ts.map +1 -1
  15. package/dist/access/index.js +1 -1
  16. package/dist/access/index.js.map +1 -1
  17. package/dist/access/types.d.ts +39 -0
  18. package/dist/access/types.d.ts.map +1 -1
  19. package/dist/config/index.d.ts +1 -1
  20. package/dist/config/index.d.ts.map +1 -1
  21. package/dist/config/types.d.ts +378 -0
  22. package/dist/config/types.d.ts.map +1 -1
  23. package/dist/context/index.d.ts +19 -1
  24. package/dist/context/index.d.ts.map +1 -1
  25. package/dist/context/index.js +153 -26
  26. package/dist/context/index.js.map +1 -1
  27. package/dist/context/nested-operations.d.ts +59 -3
  28. package/dist/context/nested-operations.d.ts.map +1 -1
  29. package/dist/context/nested-operations.js +552 -129
  30. package/dist/context/nested-operations.js.map +1 -1
  31. package/dist/context/transaction-boundary.d.ts +91 -0
  32. package/dist/context/transaction-boundary.d.ts.map +1 -0
  33. package/dist/context/transaction-boundary.js +329 -0
  34. package/dist/context/transaction-boundary.js.map +1 -0
  35. package/dist/context/write-pipeline.d.ts +15 -1
  36. package/dist/context/write-pipeline.d.ts.map +1 -1
  37. package/dist/context/write-pipeline.js +173 -10
  38. package/dist/context/write-pipeline.js.map +1 -1
  39. package/dist/fields/calendar-day.test.d.ts +2 -0
  40. package/dist/fields/calendar-day.test.d.ts.map +1 -0
  41. package/dist/fields/calendar-day.test.js +120 -0
  42. package/dist/fields/calendar-day.test.js.map +1 -0
  43. package/dist/fields/index.d.ts +18 -2
  44. package/dist/fields/index.d.ts.map +1 -1
  45. package/dist/fields/index.js +93 -17
  46. package/dist/fields/index.js.map +1 -1
  47. package/dist/hooks/index.d.ts +116 -0
  48. package/dist/hooks/index.d.ts.map +1 -1
  49. package/dist/hooks/index.js +154 -0
  50. package/dist/hooks/index.js.map +1 -1
  51. package/dist/validation/schema.test.js +222 -1
  52. package/dist/validation/schema.test.js.map +1 -1
  53. package/package.json +1 -1
  54. package/src/access/access-filter.ts +156 -0
  55. package/src/access/field-access.test.ts +255 -0
  56. package/src/access/field-access.ts +91 -5
  57. package/src/access/index.ts +1 -1
  58. package/src/access/types.ts +45 -0
  59. package/src/config/index.ts +2 -0
  60. package/src/config/types.ts +426 -0
  61. package/src/context/index.ts +207 -37
  62. package/src/context/nested-operations.ts +969 -143
  63. package/src/context/transaction-boundary.ts +440 -0
  64. package/src/context/write-pipeline.ts +234 -13
  65. package/src/fields/calendar-day.test.ts +140 -0
  66. package/src/fields/index.ts +96 -16
  67. package/src/hooks/index.ts +265 -0
  68. package/src/validation/schema.test.ts +266 -1
  69. package/tests/access.test.ts +24 -16
  70. package/tests/config.test.ts +30 -0
  71. package/tests/context.test.ts +481 -0
  72. package/tests/field-types.test.ts +17 -3
  73. package/tests/nested-access-and-hooks.test.ts +1130 -54
  74. package/tests/nested-operation-registry.test.ts +28 -3
  75. package/tests/nested-write-hooks.test.ts +864 -0
  76. package/tests/transaction-boundary-hooks.test.ts +465 -0
  77. package/tsconfig.tsbuildinfo +1 -1
@@ -150,6 +150,125 @@ export type FieldAfterOperationHookArgs<
150
150
  context: import('../access/types.js').AccessContext
151
151
  }
152
152
 
153
+ /**
154
+ * Arguments for field-level beforeTransaction hook (#590 / ADR-0010).
155
+ *
156
+ * Transaction-boundary hooks run OUTSIDE the write's database transaction.
157
+ * `beforeTransaction` runs before the transaction opens, so it has the input
158
+ * data but no persisted `item` yet (and, for create, no `item` to read). For
159
+ * update/delete the existing `item` is best-effort: present for the top-level
160
+ * target (which the pipeline resolves before opening the transaction) and
161
+ * `undefined` for nested targets (not resolved at the boundary to avoid
162
+ * pre-transaction reads). Use it for non-transactional side effects (e.g.
163
+ * external API calls) whose compensation pairs with `afterTransaction`.
164
+ */
165
+ export type FieldBeforeTransactionHookArgs<
166
+ TTypeInfo extends TypeInfo,
167
+ TFieldKey extends FieldKeys<TTypeInfo['fields']> = FieldKeys<TTypeInfo['fields']>,
168
+ > =
169
+ | {
170
+ listKey: string
171
+ fieldKey: TFieldKey
172
+ operation: 'create'
173
+ inputData: TTypeInfo['inputs']['create']
174
+ context: import('../access/types.js').AccessContext
175
+ }
176
+ | {
177
+ listKey: string
178
+ fieldKey: TFieldKey
179
+ operation: 'update'
180
+ inputData: TTypeInfo['inputs']['update']
181
+ item: TTypeInfo['item'] | undefined
182
+ context: import('../access/types.js').AccessContext
183
+ }
184
+ | {
185
+ listKey: string
186
+ fieldKey: TFieldKey
187
+ operation: 'delete'
188
+ item: TTypeInfo['item'] | undefined
189
+ context: import('../access/types.js').AccessContext
190
+ }
191
+
192
+ /**
193
+ * Arguments for field-level afterTransaction hook (#590 / ADR-0010).
194
+ *
195
+ * Runs AFTER the transaction settles and ALWAYS runs when its paired
196
+ * `beforeTransaction` ran (symmetric bracket). The `status` discriminant tells
197
+ * the hook whether the write committed or rolled back:
198
+ * - `committed`: the persisted `item`/`originalItem` are populated ONLY for the
199
+ * TOP-LEVEL record of the write. For NESTED lists they are `undefined` — the
200
+ * per-record persisted row is not reliably recoverable outside the
201
+ * transaction, and these hooks fire at per-(list, operation) granularity, not
202
+ * per record. For per-record nested compensation use the in-transaction
203
+ * `afterOperation` (which receives the correct nested `item`).
204
+ * - `rolled-back`: NO persisted `item`; the hook gets `inputData` and the
205
+ * `error` that caused the rollback so it can compensate for whatever
206
+ * `beforeTransaction` did externally.
207
+ */
208
+ export type FieldAfterTransactionHookArgs<
209
+ TTypeInfo extends TypeInfo,
210
+ TFieldKey extends FieldKeys<TTypeInfo['fields']> = FieldKeys<TTypeInfo['fields']>,
211
+ > =
212
+ | {
213
+ listKey: string
214
+ fieldKey: TFieldKey
215
+ operation: 'create'
216
+ status: 'committed'
217
+ inputData: TTypeInfo['inputs']['create']
218
+ /** Persisted row — populated for the top-level list only; `undefined` for nested lists. */
219
+ item: TTypeInfo['item'] | undefined
220
+ context: import('../access/types.js').AccessContext
221
+ }
222
+ | {
223
+ listKey: string
224
+ fieldKey: TFieldKey
225
+ operation: 'create'
226
+ status: 'rolled-back'
227
+ inputData: TTypeInfo['inputs']['create']
228
+ error: unknown
229
+ context: import('../access/types.js').AccessContext
230
+ }
231
+ | {
232
+ listKey: string
233
+ fieldKey: TFieldKey
234
+ operation: 'update'
235
+ status: 'committed'
236
+ inputData: TTypeInfo['inputs']['update']
237
+ /** Pre-write row — populated for the top-level list only; `undefined` for nested lists. */
238
+ originalItem: TTypeInfo['item'] | undefined
239
+ /** Persisted row — populated for the top-level list only; `undefined` for nested lists. */
240
+ item: TTypeInfo['item'] | undefined
241
+ context: import('../access/types.js').AccessContext
242
+ }
243
+ | {
244
+ listKey: string
245
+ fieldKey: TFieldKey
246
+ operation: 'update'
247
+ status: 'rolled-back'
248
+ inputData: TTypeInfo['inputs']['update']
249
+ originalItem: TTypeInfo['item'] | undefined
250
+ error: unknown
251
+ context: import('../access/types.js').AccessContext
252
+ }
253
+ | {
254
+ listKey: string
255
+ fieldKey: TFieldKey
256
+ operation: 'delete'
257
+ status: 'committed'
258
+ /** Pre-write row — populated for the top-level list only; `undefined` for nested lists. */
259
+ originalItem: TTypeInfo['item'] | undefined
260
+ context: import('../access/types.js').AccessContext
261
+ }
262
+ | {
263
+ listKey: string
264
+ fieldKey: TFieldKey
265
+ operation: 'delete'
266
+ status: 'rolled-back'
267
+ originalItem: TTypeInfo['item'] | undefined
268
+ error: unknown
269
+ context: import('../access/types.js').AccessContext
270
+ }
271
+
153
272
  /**
154
273
  * Arguments for field-level resolveOutput hook
155
274
  * Used to transform field values after database read
@@ -278,6 +397,53 @@ export type FieldHooks<
278
397
  */
279
398
  afterOperation?: (args: FieldAfterOperationHookArgs<TTypeInfo, TFieldKey>) => Promise<void> | void
280
399
 
400
+ /**
401
+ * Perform side effects BEFORE the write's database transaction opens
402
+ * (#590 / ADR-0010 transaction-boundary hook).
403
+ *
404
+ * Unlike `beforeOperation` (which runs INSIDE the transaction and rolls back
405
+ * with it), this runs OUTSIDE the transaction — use it for non-transactional
406
+ * side effects such as external API calls that must not hold a DB transaction
407
+ * open and cannot be rolled back. If this throws, the write is aborted (the
408
+ * transaction never opens) and the paired `afterTransaction` fires with
409
+ * `status: 'rolled-back'`.
410
+ *
411
+ * @example
412
+ * ```typescript
413
+ * beforeTransaction: async ({ operation, inputData }) => {
414
+ * await externalApi.reserve(inputData.externalId)
415
+ * }
416
+ * ```
417
+ */
418
+ beforeTransaction?: (
419
+ args: FieldBeforeTransactionHookArgs<TTypeInfo, TFieldKey>,
420
+ ) => Promise<void> | void
421
+
422
+ /**
423
+ * Perform side effects AFTER the write's database transaction settles
424
+ * (#590 / ADR-0010 transaction-boundary hook).
425
+ *
426
+ * ALWAYS runs when the paired `beforeTransaction` ran (symmetric bracket),
427
+ * receiving `status: 'committed' | 'rolled-back'`. On `committed` it gets the
428
+ * persisted `item` ONLY for the top-level record (`undefined` for nested
429
+ * lists — use the in-transaction `afterOperation` for per-record nested
430
+ * compensation); on `rolled-back` it gets the `error` that caused the
431
+ * rollback and NO `item`, so it can compensate for whatever `beforeTransaction`
432
+ * did externally.
433
+ *
434
+ * @example
435
+ * ```typescript
436
+ * afterTransaction: async (args) => {
437
+ * if (args.status === 'rolled-back') {
438
+ * await externalApi.release(args.inputData.externalId)
439
+ * }
440
+ * }
441
+ * ```
442
+ */
443
+ afterTransaction?: (
444
+ args: FieldAfterTransactionHookArgs<TTypeInfo, TFieldKey>,
445
+ ) => Promise<void> | void
446
+
281
447
  /**
282
448
  * Transform field value after database read
283
449
  * Called when returning results from query operations
@@ -1269,6 +1435,117 @@ export type AfterOperationHookArgs<
1269
1435
  context: import('../access/types.js').AccessContext
1270
1436
  }
1271
1437
 
1438
+ /**
1439
+ * Hook arguments for the list-level beforeTransaction hook (#590 / ADR-0010).
1440
+ *
1441
+ * Runs BEFORE the write's transaction opens (outside it). Has input data but no
1442
+ * persisted `item`. For update/delete the existing `item` is best-effort:
1443
+ * present for the top-level target (resolved before the transaction opens) and
1444
+ * `undefined` for nested targets. For non-transactional side effects whose
1445
+ * compensation pairs with `afterTransaction`.
1446
+ */
1447
+ export type BeforeTransactionHookArgs<
1448
+ TOutput = Record<string, unknown>,
1449
+ TCreateInput = Record<string, unknown>,
1450
+ TUpdateInput = Record<string, unknown>,
1451
+ > =
1452
+ | {
1453
+ listKey: string
1454
+ operation: 'create'
1455
+ inputData: TCreateInput
1456
+ context: import('../access/types.js').AccessContext
1457
+ }
1458
+ | {
1459
+ listKey: string
1460
+ operation: 'update'
1461
+ inputData: TUpdateInput
1462
+ item: TOutput | undefined
1463
+ context: import('../access/types.js').AccessContext
1464
+ }
1465
+ | {
1466
+ listKey: string
1467
+ operation: 'delete'
1468
+ item: TOutput | undefined
1469
+ context: import('../access/types.js').AccessContext
1470
+ }
1471
+
1472
+ /**
1473
+ * Hook arguments for the list-level afterTransaction hook (#590 / ADR-0010).
1474
+ *
1475
+ * Runs AFTER the write's transaction settles and ALWAYS runs when the paired
1476
+ * `beforeTransaction` ran (symmetric bracket). The `status` discriminant tells
1477
+ * the hook whether the write committed or rolled back:
1478
+ * - `committed`: the persisted `item`/`originalItem` are populated ONLY for the
1479
+ * TOP-LEVEL record of the write. For NESTED lists they are `undefined` — the
1480
+ * per-record persisted row is not reliably recoverable outside the
1481
+ * transaction (recovering it would duplicate #569's in-transaction id-diff
1482
+ * machinery), and these hooks fire at per-(list, operation) granularity, not
1483
+ * per record. For per-record nested compensation use the in-transaction
1484
+ * `afterOperation` (which receives the correct nested `item`);
1485
+ * transaction-boundary hooks are for external-call compensation keyed off
1486
+ * `status`/`inputData`.
1487
+ * - `rolled-back`: NO persisted `item`; the hook gets `inputData` and the
1488
+ * `error` that caused the rollback so it can compensate.
1489
+ */
1490
+ export type AfterTransactionHookArgs<
1491
+ TOutput = Record<string, unknown>,
1492
+ TCreateInput = Record<string, unknown>,
1493
+ TUpdateInput = Record<string, unknown>,
1494
+ > =
1495
+ | {
1496
+ listKey: string
1497
+ operation: 'create'
1498
+ status: 'committed'
1499
+ inputData: TCreateInput
1500
+ /** Persisted row — populated for the top-level list only; `undefined` for nested lists. */
1501
+ item: TOutput | undefined
1502
+ context: import('../access/types.js').AccessContext
1503
+ }
1504
+ | {
1505
+ listKey: string
1506
+ operation: 'create'
1507
+ status: 'rolled-back'
1508
+ inputData: TCreateInput
1509
+ error: unknown
1510
+ context: import('../access/types.js').AccessContext
1511
+ }
1512
+ | {
1513
+ listKey: string
1514
+ operation: 'update'
1515
+ status: 'committed'
1516
+ inputData: TUpdateInput
1517
+ /** Pre-write row — populated for the top-level list only; `undefined` for nested lists. */
1518
+ originalItem: TOutput | undefined
1519
+ /** Persisted row — populated for the top-level list only; `undefined` for nested lists. */
1520
+ item: TOutput | undefined
1521
+ context: import('../access/types.js').AccessContext
1522
+ }
1523
+ | {
1524
+ listKey: string
1525
+ operation: 'update'
1526
+ status: 'rolled-back'
1527
+ inputData: TUpdateInput
1528
+ originalItem: TOutput | undefined
1529
+ error: unknown
1530
+ context: import('../access/types.js').AccessContext
1531
+ }
1532
+ | {
1533
+ listKey: string
1534
+ operation: 'delete'
1535
+ status: 'committed'
1536
+ /** Pre-write row — populated for the top-level list only; `undefined` for nested lists. */
1537
+ originalItem: TOutput | undefined
1538
+ context: import('../access/types.js').AccessContext
1539
+ }
1540
+ | {
1541
+ listKey: string
1542
+ operation: 'delete'
1543
+ status: 'rolled-back'
1544
+ originalItem: TOutput | undefined
1545
+ error: unknown
1546
+ context: import('../access/types.js').AccessContext
1547
+ }
1548
+
1272
1549
  export type Hooks<
1273
1550
  TOutput = Record<string, unknown>,
1274
1551
  TCreateInput = Record<string, unknown>,
@@ -1284,6 +1561,26 @@ export type Hooks<
1284
1561
  afterOperation?: (
1285
1562
  args: AfterOperationHookArgs<TOutput, TCreateInput, TUpdateInput>,
1286
1563
  ) => Promise<void>
1564
+ /**
1565
+ * Side effect BEFORE the write's transaction opens (#590 / ADR-0010).
1566
+ * Runs OUTSIDE the transaction — for non-transactional work (external API
1567
+ * calls). Throwing aborts the write; the paired `afterTransaction` then fires
1568
+ * with `status: 'rolled-back'`. See {@link BeforeTransactionHookArgs}.
1569
+ */
1570
+ beforeTransaction?: (
1571
+ args: BeforeTransactionHookArgs<TOutput, TCreateInput, TUpdateInput>,
1572
+ ) => Promise<void> | void
1573
+ /**
1574
+ * Side effect AFTER the write's transaction settles (#590 / ADR-0010).
1575
+ * ALWAYS runs when `beforeTransaction` ran; receives `committed | rolled-back`
1576
+ * + `error`. The persisted `item`/`originalItem` are present only on commit
1577
+ * AND only for the top-level record (`undefined` for nested lists). The
1578
+ * compensation half of the transaction-boundary bracket. See
1579
+ * {@link AfterTransactionHookArgs}.
1580
+ */
1581
+ afterTransaction?: (
1582
+ args: AfterTransactionHookArgs<TOutput, TCreateInput, TUpdateInput>,
1583
+ ) => Promise<void> | void
1287
1584
  /**
1288
1585
  * @deprecated Use 'validate' instead. This alias is provided for backwards compatibility.
1289
1586
  */
@@ -1402,6 +1699,68 @@ export type ListConfig<TTypeInfo extends TypeInfo> = {
1402
1699
  */
1403
1700
  autoCreate?: boolean
1404
1701
  }
1702
+ /**
1703
+ * UI configuration for this list (admin interface).
1704
+ *
1705
+ * Mirrors Keystone's list-level `ui` block. Currently only `listView`
1706
+ * defaults (columns + sort) are supported.
1707
+ */
1708
+ ui?: ListUIConfig
1709
+ }
1710
+
1711
+ /**
1712
+ * List-level UI configuration for the admin interface.
1713
+ *
1714
+ * Mirrors Keystone's `ui` block on a list. Only the list-view defaults
1715
+ * (column selection/order and default sort) are supported today; other
1716
+ * Keystone concerns (`label`, `labelField`, `description`) are intentionally
1717
+ * deferred as they cover different concerns (navigation text and
1718
+ * relationship-picker labels rather than list-view defaults).
1719
+ */
1720
+ export type ListUIConfig = {
1721
+ /**
1722
+ * Default list-view (table) configuration for this list, mirroring
1723
+ * Keystone's `ui.listView`.
1724
+ */
1725
+ listView?: ListViewUIConfig
1726
+ }
1727
+
1728
+ /**
1729
+ * Default list-view (table) configuration for a list, mirroring Keystone's
1730
+ * `ui.listView`.
1731
+ *
1732
+ * When omitted, the admin UI falls back to its existing defaults: every
1733
+ * non-system field is shown as a column and no default sort is applied.
1734
+ */
1735
+ export type ListViewUIConfig = {
1736
+ /**
1737
+ * The fields to show as columns in the list table, in order.
1738
+ *
1739
+ * Drives both the column **selection** and their **order**. When omitted,
1740
+ * all non-system fields are shown (current default behaviour).
1741
+ *
1742
+ * @example
1743
+ * ```typescript
1744
+ * ui: { listView: { initialColumns: ['title', 'status', 'author'] } }
1745
+ * ```
1746
+ */
1747
+ initialColumns?: string[]
1748
+ /**
1749
+ * The default sort applied to the list table.
1750
+ *
1751
+ * When omitted, no default sort is applied (current default behaviour).
1752
+ *
1753
+ * @example
1754
+ * ```typescript
1755
+ * ui: { listView: { initialSort: { field: 'createdAt', direction: 'desc' } } }
1756
+ * ```
1757
+ */
1758
+ initialSort?: {
1759
+ /** The field to sort by. Must be a field defined on the list. */
1760
+ field: string
1761
+ /** The sort direction. */
1762
+ direction: 'asc' | 'desc'
1763
+ }
1405
1764
  }
1406
1765
 
1407
1766
  /**
@@ -1611,6 +1970,40 @@ export type DatabaseConfig = {
1611
1970
  * ```
1612
1971
  */
1613
1972
  extendPrismaSchema?: (schema: string) => string
1973
+ /**
1974
+ * Override the Prisma `generator client { ... }` options the CLI emits for the
1975
+ * `.opensaas` prisma-client subtree.
1976
+ *
1977
+ * By default the generator emits `importFileExtension = "ts"` and
1978
+ * `moduleFormat = "esm"` so the whole generated bundle is statically
1979
+ * resolvable and matches the explicit `.ts` import-extension style the rest of
1980
+ * the `.opensaas` bundle uses (see ADR-0008). Supply this option only when you
1981
+ * need a different module/extension story (e.g. emitting `.js` extensions for a
1982
+ * Node-only consumer). Any value you provide wins; omitted keys fall back to
1983
+ * the `ts`/`esm` defaults.
1984
+ *
1985
+ * @example Emit `.js` extensions and CommonJS for a plain-Node consumer
1986
+ * ```typescript
1987
+ * db: {
1988
+ * provider: 'postgresql',
1989
+ * prismaGeneratorOptions: {
1990
+ * importFileExtension: 'js',
1991
+ * moduleFormat: 'commonjs',
1992
+ * },
1993
+ * // ... rest of config
1994
+ * }
1995
+ * ```
1996
+ */
1997
+ prismaGeneratorOptions?: {
1998
+ /**
1999
+ * Value for the generator's `importFileExtension` option. Defaults to `'ts'`.
2000
+ */
2001
+ importFileExtension?: 'ts' | 'js'
2002
+ /**
2003
+ * Value for the generator's `moduleFormat` option. Defaults to `'esm'`.
2004
+ */
2005
+ moduleFormat?: 'esm' | 'commonjs'
2006
+ }
1614
2007
  }
1615
2008
 
1616
2009
  /**
@@ -2096,6 +2489,39 @@ export interface OutputConfig {
2096
2489
  * @default ".opensaas"
2097
2490
  */
2098
2491
  opensaasDir?: string
2492
+ /**
2493
+ * Opt in to an additional **Node build** of the Generated bundle.
2494
+ *
2495
+ * By default (omitted) the generator emits only the bundler-loadable `.ts`
2496
+ * form (ADR-0008): TypeScript with explicit `.ts` import extensions, traced
2497
+ * and transpiled by the host's bundler. That form cannot execute under plain
2498
+ * Node, so a live module that must run in BOTH a bundled and a bundler-less
2499
+ * runtime (e.g. better-auth's Prisma adapter, imported by the Next server AND
2500
+ * by a Playwright e2e helper or a build-time script) has no Node-loadable
2501
+ * entry to point at.
2502
+ *
2503
+ * Setting `buildTarget: 'node'` additionally compiles the bundle to a
2504
+ * plain-Node-loadable ESM form under `<opensaasDir>/dist/` (`.js` + `.d.ts`,
2505
+ * with a `{"type":"module"}` marker). The compiled entry is
2506
+ * `<opensaasDir>/dist/context.js`; a portable module imports it directly so
2507
+ * the bundler traces it AND plain Node executes it (one specifier, both
2508
+ * runtimes — see ADR-0011). The default `.ts` form is unchanged and still
2509
+ * emitted; the Node build is purely additive.
2510
+ *
2511
+ * `'node'` is the only target today. The field is a string-literal union so
2512
+ * future compiled targets can be added without a breaking change.
2513
+ *
2514
+ * @example
2515
+ * ```typescript
2516
+ * export default config({
2517
+ * output: { buildTarget: 'node' },
2518
+ * // ...
2519
+ * })
2520
+ * // Then import the compiled entry from a plain-Node consumer:
2521
+ * // const { rawOpensaasContext } = await import('./.opensaas/dist/context.js')
2522
+ * ```
2523
+ */
2524
+ buildTarget?: 'node'
2099
2525
  }
2100
2526
 
2101
2527
  export interface OpenSaasConfig {