@aventara/client 1.0.0-rc.10 → 1.0.0-rc.12

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.
@@ -227,7 +227,7 @@ export async function askCan(
227
227
  : question,
228
228
  ),
229
229
  });
230
- const response = await post(connection, "/_access/can", operation, headers, body, options);
230
+ const response = await post(connection, "/_access/can", operation, headers, body, options.signal);
231
231
  return decoded(operation, response.status, () => {
232
232
  const data: unknown = response.data;
233
233
  if (!Array.isArray(data) || data.length !== questions.length || !data.every((answer) => typeof answer === "boolean")) {
@@ -269,7 +269,8 @@ import {
269
269
  } from ${from("./errors")};
270
270
 
271
271
  /**
272
- * The transport of Aventara protocol version 1: one POST per operation,
272
+ * The transport of Aventara protocol version 1: one POST per operation, or one
273
+ * POST /_all for an Operation Set of them,
273
274
  * sent with the protocol version and ClientContract hash, its response
274
275
  * read by the protocol's rule: A1xxx resolves with the envelope's data, every other
275
276
  * code throws its AvError subclass, and anything that is not a framework
@@ -297,6 +298,9 @@ const FRAMEWORK_HEADERS: Readonly<Record<string, string>> = {
297
298
  [WIRE_PREFIX + ${JSON.stringify(IDENTITY_HEADERS.contractHash)}]: CLIENT_CONTRACT_HASH,
298
299
  };
299
300
 
301
+ /** The media type of a streamed Operation Set answer: one JSON line per entry. */
302
+ const NDJSON = "application/x-ndjson";
303
+
300
304
  /** The request id header: AvCallOptions.requestId sends it. */
301
305
  const REQUEST_ID_HEADER = WIRE_PREFIX + ${JSON.stringify(IDENTITY_HEADERS.requestId)};
302
306
 
@@ -307,9 +311,11 @@ export interface StructuralSignal {
307
311
 
308
312
  /** The request the transport hands to fetch. */
309
313
  export interface FetchInit {
310
- readonly method: "POST";
314
+ /** POST, or GET for a cached read. */
315
+ readonly method: "POST" | "GET";
311
316
  readonly headers: Readonly<Record<string, string>>;
312
- readonly body: string;
317
+ /** The JSON body of a POST; a GET has none. */
318
+ readonly body?: string;
313
319
  readonly signal?: PlatformSignal;
314
320
  }
315
321
 
@@ -317,6 +323,16 @@ export interface FetchInit {
317
323
  export interface FetchResponse {
318
324
  readonly status: number;
319
325
  text(): Promise<string>;
326
+ /** Read for an Operation Set's answer: its media type. */
327
+ readonly headers?: { get(name: string): string | null };
328
+ /** Read for a streamed Operation Set answer, where the runtime gives one. */
329
+ readonly body?: { getReader(): BodyReader } | null;
330
+ }
331
+
332
+ /** What the transport reads of a response body's stream. */
333
+ export interface BodyReader {
334
+ read(): Promise<{ readonly done: boolean; readonly value?: Uint8Array | undefined }>;
335
+ cancel(reason?: unknown): Promise<void>;
320
336
  }
321
337
 
322
338
  /** The AvFetch API as far as the transport calls it, where the consumer's lib declares none. */
@@ -346,15 +362,30 @@ export interface TransportConnection {
346
362
 
347
363
  /** Per-call options. Never serialized into the operation body. */
348
364
  export interface AvCallOptions {
349
- /** Aborts the request; the call rejects with what fetch rejected with. */
365
+ /**
366
+ * Aborts this call, and only this call. Sent in an Operation Set, the call
367
+ * rejects with the signal's reason (an AbortError) and the others go on; sent
368
+ * alone, it aborts the request, and the call rejects with what fetch rejected
369
+ * with. Aborted before it is sent, it is never sent.
370
+ */
350
371
  readonly signal?: PlatformSignal;
351
- /** Extra request headers. The framework's own headers cannot be set here. */
372
+ /**
373
+ * Extra request headers. The framework's own headers cannot be set here. A
374
+ * call with headers is sent alone, never in an Operation Set.
375
+ */
352
376
  readonly headers?: Readonly<Record<string, string>>;
353
377
  /**
354
378
  * Sent as the request id header (Aventara-Request-Id); the empty string is
355
- * absent. It wins over that header in headers, which it overwrites.
379
+ * absent. It wins over that header in headers, which it overwrites. A call
380
+ * with a request id is sent alone, never in an Operation Set.
356
381
  */
357
382
  readonly requestId?: string;
383
+ /**
384
+ * false sends this operation call to its own route, never in an Operation Set
385
+ * (POST /_all) with the calls made beside it. A transaction is always
386
+ * sent alone.
387
+ */
388
+ readonly grouping?: boolean;
358
389
  }
359
390
 
360
391
  type SuccessCode = Exclude<AvOperationCode, AvOperationErrorCode>;
@@ -405,6 +436,80 @@ export function refuseUnsentValues(
405
436
  );
406
437
  }
407
438
 
439
+ /** One operation, checked and encoded when it was called: what any request for it sends. */
440
+ export interface PreparedOperation {
441
+ readonly resource: string;
442
+ readonly family: string;
443
+ readonly variant: string;
444
+ /** How a failure names it: \`"Post" find.many\`. */
445
+ readonly label: string;
446
+ /** Its request headers: the framework's, and the caller's when it is sent alone. */
447
+ readonly headers: Readonly<Record<string, string>>;
448
+ /** The caller's headers alone (its request id among them): what a cached read's GET sends. */
449
+ readonly callerHeaders: Readonly<Record<string, string>>;
450
+ /** Its arguments in wire form, as JSON text: its own route's body. */
451
+ readonly body: string;
452
+ }
453
+
454
+ /**
455
+ * Checks and encodes one operation call, sending nothing: the checks
456
+ * {@link execute} runs before it sends, in its order, and with its errors.
457
+ */
458
+ export function prepareOperation(
459
+ resource: string,
460
+ family: string,
461
+ variant: string,
462
+ args: Readonly<Record<string, unknown>>,
463
+ options: AvCallOptions = {},
464
+ check?: (resource: string, family: string, args: unknown) => void,
465
+ ): PreparedOperation {
466
+ refuseUnsentValues(resource, family, variant, args, ["arguments"]);
467
+ check?.(resource, family, args);
468
+ const label = JSON.stringify(resource) + " " + family + "." + variant;
469
+ const callerHeaders = callerHeadersOf(options);
470
+ const headers = { ...FRAMEWORK_HEADERS, ...callerHeaders };
471
+ const body = serializeWireBody(encodeArguments(argumentObject(args)));
472
+ return { resource, family, variant, label, headers, callerHeaders, body };
473
+ }
474
+
475
+ /**
476
+ * Sends one prepared operation to its own route and settles its envelope.
477
+ *
478
+ * @throws as {@link execute} does once its arguments are checked.
479
+ */
480
+ export async function sendOperation(
481
+ connection: TransportConnection,
482
+ operation: PreparedOperation,
483
+ signal?: PlatformSignal,
484
+ ): Promise<unknown> {
485
+ const response = await post(connection, routeOf(operation), operation.label, operation.headers, operation.body, signal);
486
+ return decoded(operation.label, response.status, () => decodeResult(DECODE_TABLE, operation.resource, response.data));
487
+ }
488
+
489
+ /** An operation's own route: \`/_resources/<resource>/<family>/<variant>\`. */
490
+ export function routeOf(operation: PreparedOperation): string {
491
+ return "/_resources/" + [operation.resource, operation.family, operation.variant].map(encodeURIComponent).join("/");
492
+ }
493
+
494
+ /**
495
+ * Sends one cached read as a GET to its own route with \`query\` (its \`q\` and
496
+ * \`v\`) and only the caller's headers, and settles its envelope as
497
+ * {@link sendOperation} does.
498
+ */
499
+ export async function sendRead(
500
+ connection: TransportConnection,
501
+ operation: PreparedOperation,
502
+ query: string,
503
+ signal?: PlatformSignal,
504
+ ): Promise<unknown> {
505
+ const init: FetchInit =
506
+ signal === undefined
507
+ ? { method: "GET", headers: operation.callerHeaders }
508
+ : { method: "GET", headers: operation.callerHeaders, signal };
509
+ const response = await received(connection, routeOf(operation) + "?" + query, operation.label, init);
510
+ return decoded(operation.label, response.status, () => decodeResult(DECODE_TABLE, operation.resource, response.data));
511
+ }
512
+
408
513
  /**
409
514
  * Runs one operation: POSTs its arguments and settles its envelope.
410
515
  *
@@ -428,14 +533,7 @@ export async function execute(
428
533
  options: AvCallOptions = {},
429
534
  check?: (resource: string, family: string, args: unknown) => void,
430
535
  ): Promise<unknown> {
431
- refuseUnsentValues(resource, family, variant, args, ["arguments"]);
432
- check?.(resource, family, args);
433
- const operation = JSON.stringify(resource) + " " + family + "." + variant;
434
- const headers = requestHeaders(options);
435
- const body = serializeWireBody(encodeArguments(argumentObject(args)));
436
- const path = "/_resources/" + [resource, family, variant].map(encodeURIComponent).join("/");
437
- const response = await post(connection, path, operation, headers, body, options);
438
- return decoded(operation, response.status, () => decodeResult(DECODE_TABLE, resource, response.data));
536
+ return sendOperation(connection, prepareOperation(resource, family, variant, args, options, check), options.signal);
439
537
  }
440
538
 
441
539
  /**
@@ -455,7 +553,7 @@ export async function executePlan(
455
553
  const operation = "the transaction";
456
554
  const headers = requestHeaders(options);
457
555
  const body = serializeWireBody(plan);
458
- const response = await post(connection, "/_transactions", operation, headers, body, options);
556
+ const response = await post(connection, "/_transactions", operation, headers, body, options.signal);
459
557
  return decoded(operation, response.status, () => {
460
558
  if (!Array.isArray(response.data) || response.data.length !== resources.length) {
461
559
  throw new TypeError("A committed plan's data must hold one result per operation.");
@@ -471,38 +569,279 @@ async function post(
471
569
  operation: string,
472
570
  headers: Readonly<Record<string, string>>,
473
571
  body: string,
474
- options: AvCallOptions,
572
+ signal: PlatformSignal | undefined,
573
+ ): Promise<{ readonly status: number; readonly data: unknown }> {
574
+ const init: FetchInit = signal === undefined ? { method: "POST", headers, body } : { method: "POST", headers, body, signal };
575
+ return received(connection, path, operation, init);
576
+ }
577
+
578
+ /** One request and its settled envelope data, still in wire form. */
579
+ async function received(
580
+ connection: TransportConnection,
581
+ path: string,
582
+ operation: string,
583
+ init: FetchInit,
475
584
  ): Promise<{ readonly status: number; readonly data: unknown }> {
585
+ const signal = init.signal;
586
+ const response = await send(connection, path, operation, init);
587
+ let text: string;
588
+ try {
589
+ text = await response.text();
590
+ } catch (error) {
591
+ if (signal?.aborted === true) {
592
+ throw error;
593
+ }
594
+ throw new AvTransportError(
595
+ "The response to " + operation + " (HTTP " + response.status + ") could not be read.",
596
+ response.status,
597
+ );
598
+ }
599
+ return { status: response.status, data: settle(operation, response.status, text) };
600
+ }
601
+
602
+ /** One request, until its response arrives; a caller's abort rejects with what fetch rejected with. */
603
+ async function send(
604
+ connection: TransportConnection,
605
+ path: string,
606
+ operation: string,
607
+ init: FetchInit,
608
+ ): Promise<FetchResponse> {
476
609
  const url = connection.entrypoint + path;
477
- const init: FetchInit =
478
- options.signal === undefined
479
- ? { method: "POST", headers, body }
480
- : { method: "POST", headers, body, signal: options.signal };
610
+ const signal = init.signal;
481
611
 
482
612
  // Called unbound: a browser's fetch refuses a this that is not the global.
483
613
  const fetch = connection.fetch as StructuralFetch;
484
- let response: FetchResponse;
485
614
  try {
486
- response = await fetch(url, init);
615
+ return await fetch(url, init);
487
616
  } catch (error) {
488
- if (options.signal?.aborted === true) {
617
+ if (signal?.aborted === true) {
489
618
  throw error;
490
619
  }
491
620
  throw new AvTransportError("The request for " + operation + " failed before any response arrived.", null);
492
621
  }
493
- let text: string;
622
+ }
623
+
624
+ /** How one entry of an Operation Set settled: its decoded data, or what its call rejects with. */
625
+ export type EntryOutcome =
626
+ | { readonly kind: "value"; readonly value: unknown }
627
+ | { readonly kind: "error"; readonly error: unknown };
628
+
629
+ /** What an Operation Set request came to: its entries answered, or no /_all on the server (A2002, A2000). */
630
+ export type OperationSetAnswer = "answered" | "absent";
631
+
632
+ /** How errors name an Operation Set request. */
633
+ const SET_LABEL = "the Operation Set";
634
+
635
+ /** The key the client gives the entry at \`index\`: an identifier, never the caller's. */
636
+ export function operationSetKey(index: number): string {
637
+ return "k" + index;
638
+ }
639
+
640
+ /** One entry of an Operation Set's body, as JSON text, under \`key\`. */
641
+ export function operationSetEntry(key: string, operation: PreparedOperation): string {
642
+ return (
643
+ JSON.stringify(key) +
644
+ ':{"resource":' +
645
+ JSON.stringify(operation.resource) +
646
+ ',"family":' +
647
+ JSON.stringify(operation.family) +
648
+ ',"variant":' +
649
+ JSON.stringify(operation.variant) +
650
+ ',"args":' +
651
+ operation.body +
652
+ "}"
653
+ );
654
+ }
655
+
656
+ /** The JSON text of an Operation Set's body, around its entries' text. */
657
+ export function operationSetBody(entries: readonly string[]): string {
658
+ return '{"operations":{' + entries.join(",") + "}}";
659
+ }
660
+
661
+ /**
662
+ * Sends prepared operations as one Operation Set (\`POST /_all\`), each under a key
663
+ * the client makes, and hands each entry's outcome to \`answer\` as it arrives: line
664
+ * by line when the answer is streamed (NDJSON, asked for where the runtime can
665
+ * stream), all at once from a plain JSON answer. Each entry decodes and fails as
666
+ * its own route would answer it. A request refused whole answers every entry with
667
+ * that refusal, except A2002 or A2000, which answer none: the server has no /_all.
668
+ * A response that is not an Aventara answer answers each entry still waiting with
669
+ * AvTransportError. Never rejects.
670
+ */
671
+ export async function sendOperationSet(
672
+ connection: TransportConnection,
673
+ operations: readonly PreparedOperation[],
674
+ signal: PlatformSignal | undefined,
675
+ answer: (index: number, outcome: EntryOutcome) => void,
676
+ ): Promise<OperationSetAnswer> {
677
+ const keys = new Map(operations.map((_, index) => [operationSetKey(index), index]));
678
+ const waiting = new Set(keys.values());
679
+ const settleEntry = (index: number, outcome: EntryOutcome): void => {
680
+ if (waiting.delete(index)) {
681
+ answer(index, outcome);
682
+ }
683
+ };
684
+ const failWaiting = (error: unknown): OperationSetAnswer => {
685
+ for (const index of [...waiting]) {
686
+ settleEntry(index, { kind: "error", error });
687
+ }
688
+ return "answered";
689
+ };
690
+ const entryOf = (index: number, status: number, envelope: unknown): EntryOutcome => {
691
+ const operation = operations[index] as PreparedOperation;
692
+ try {
693
+ const data = settleEnvelope(operation.label, status, envelope);
694
+ return { kind: "value", value: decoded(operation.label, status, () => decodeResult(DECODE_TABLE, operation.resource, data)) };
695
+ } catch (error) {
696
+ return { kind: "error", error };
697
+ }
698
+ };
699
+ const streaming = canStream();
700
+ const headers: Record<string, string> = streaming ? { ...FRAMEWORK_HEADERS, Accept: NDJSON } : { ...FRAMEWORK_HEADERS };
701
+ const body = operationSetBody(operations.map((operation, index) => operationSetEntry(operationSetKey(index), operation)));
702
+
703
+ let response: FetchResponse;
494
704
  try {
495
- text = await response.text();
705
+ response = await send(
706
+ connection,
707
+ "/_all",
708
+ SET_LABEL,
709
+ signal === undefined ? { method: "POST", headers, body } : { method: "POST", headers, body, signal },
710
+ );
496
711
  } catch (error) {
497
- if (options.signal?.aborted === true) {
498
- throw error;
499
- }
500
- throw new AvTransportError(
501
- "The response to " + operation + " (HTTP " + response.status + ") could not be read.",
712
+ return failWaiting(error);
713
+ }
714
+ const unreadable = (): AvTransportError =>
715
+ new AvTransportError("The response to " + SET_LABEL + " (HTTP " + response.status + ") could not be read.", response.status);
716
+ const malformed = (): AvTransportError =>
717
+ new AvTransportError(
718
+ "The response to " + SET_LABEL + " (HTTP " + response.status + ") is not an Aventara Operation Set answer.",
502
719
  response.status,
503
720
  );
721
+
722
+ const mediaType = (response.headers?.get("content-type") ?? "").split(";")[0]?.trim().toLowerCase();
723
+ if (mediaType !== NDJSON) {
724
+ let text: string;
725
+ try {
726
+ text = await response.text();
727
+ } catch {
728
+ return failWaiting(unreadable());
729
+ }
730
+ const envelope = envelopeOf(parseJson(text));
731
+ if (envelope === undefined) {
732
+ return failWaiting(
733
+ new AvTransportError(
734
+ "The response to " + SET_LABEL + " (HTTP " + response.status + ") is not an Aventara response envelope.",
735
+ response.status,
736
+ ),
737
+ );
738
+ }
739
+ if (envelope.outcome === "failure") {
740
+ // A server without /_all: A2002 (no such route), or A2000 from a server older
741
+ // than Operation Sets, which reads /_all as a malformed operation path.
742
+ return envelope.code === "A2002" || envelope.code === "A2000"
743
+ ? "absent"
744
+ : failWaiting(frameworkErrorOf(envelope.code, envelope.cause));
745
+ }
746
+ const entries = envelope.data;
747
+ if (!isRecord(entries)) {
748
+ return failWaiting(malformed());
749
+ }
750
+ for (const [key, index] of keys) {
751
+ if (Object.hasOwn(entries, key)) {
752
+ settleEntry(index, entryOf(index, response.status, entries[key]));
753
+ }
754
+ }
755
+ return failWaiting(
756
+ new AvTransportError(
757
+ "The response to " + SET_LABEL + " (HTTP " + response.status + ") holds no answer for this call.",
758
+ response.status,
759
+ ),
760
+ );
504
761
  }
505
- return { status: response.status, data: settle(operation, response.status, text) };
762
+
763
+ // One line: {"key": …, "envelope": …}. false when it is not one of this request's.
764
+ const readLine = (line: string): boolean => {
765
+ if (line.trim() === "") {
766
+ return true;
767
+ }
768
+ const parsed = parseJson(line);
769
+ if (!isRecord(parsed) || !Object.hasOwn(parsed, "envelope") || typeof parsed["key"] !== "string") {
770
+ return false;
771
+ }
772
+ const index = keys.get(parsed["key"]);
773
+ if (index === undefined) {
774
+ return false;
775
+ }
776
+ settleEntry(index, entryOf(index, response.status, parsed["envelope"]));
777
+ return true;
778
+ };
779
+ const ended = (): OperationSetAnswer =>
780
+ failWaiting(
781
+ new AvTransportError(
782
+ "The response to " + SET_LABEL + " (HTTP " + response.status + ") ended before it answered this call.",
783
+ response.status,
784
+ ),
785
+ );
786
+
787
+ const reader = streaming ? response.body?.getReader() : undefined;
788
+ if (reader === undefined) {
789
+ let text: string;
790
+ try {
791
+ text = await response.text();
792
+ } catch {
793
+ return failWaiting(unreadable());
794
+ }
795
+ for (const line of text.split("\\n")) {
796
+ if (!readLine(line)) {
797
+ return failWaiting(malformed());
798
+ }
799
+ }
800
+ return ended();
801
+ }
802
+ const decoder = textDecoder();
803
+ let buffer = "";
804
+ for (;;) {
805
+ let chunk: { readonly done: boolean; readonly value?: Uint8Array | undefined };
806
+ try {
807
+ chunk = await reader.read();
808
+ } catch {
809
+ return failWaiting(unreadable());
810
+ }
811
+ buffer += decoder.decode(chunk.value, { stream: !chunk.done });
812
+ let newline = buffer.indexOf("\\n");
813
+ while (newline !== -1) {
814
+ if (!readLine(buffer.slice(0, newline))) {
815
+ reader.cancel().catch((): void => undefined);
816
+ return failWaiting(malformed());
817
+ }
818
+ buffer = buffer.slice(newline + 1);
819
+ newline = buffer.indexOf("\\n");
820
+ }
821
+ if (chunk.done) {
822
+ return readLine(buffer) ? ended() : failWaiting(malformed());
823
+ }
824
+ }
825
+ }
826
+
827
+ /** What the transport reads of a TextDecoder, where the consumer's lib declares none. */
828
+ interface StreamDecoder {
829
+ decode(input?: Uint8Array, options?: { readonly stream?: boolean }): string;
830
+ }
831
+
832
+ /** Whether this runtime can read an answer as it streams: a stream reader and a text decoder. */
833
+ function canStream(): boolean {
834
+ const platform = globalThis as {
835
+ readonly ReadableStream?: { readonly prototype?: { readonly getReader?: unknown } };
836
+ readonly TextDecoder?: unknown;
837
+ };
838
+ return typeof platform.ReadableStream?.prototype?.getReader === "function" && typeof platform.TextDecoder === "function";
839
+ }
840
+
841
+ /** The platform's TextDecoder, for UTF-8 (canStream says one exists). */
842
+ function textDecoder(): StreamDecoder {
843
+ const Platform = (globalThis as unknown as { readonly TextDecoder: new () => StreamDecoder }).TextDecoder;
844
+ return new Platform();
506
845
  }
507
846
 
508
847
  /**
@@ -529,7 +868,12 @@ function decoded<T>(operation: string, status: number, decode: () => T): T {
529
868
  * this client cannot read, which is what AvTransportError means.
530
869
  */
531
870
  function settle(operation: string, status: number, text: string): unknown {
532
- const envelope = envelopeOf(parseJson(text));
871
+ return settleEnvelope(operation, status, parseJson(text));
872
+ }
873
+
874
+ /** Settles one parsed response body (or one Operation Set entry's envelope) by the same rule. */
875
+ function settleEnvelope(operation: string, status: number, body: unknown): unknown {
876
+ const envelope = envelopeOf(body);
533
877
  if (envelope === undefined) {
534
878
  throw new AvTransportError(
535
879
  "The response to " + operation + " (HTTP " + status + ") is not an Aventara response envelope.",
@@ -680,13 +1024,17 @@ function argumentObject(args: unknown): unknown {
680
1024
  return args;
681
1025
  }
682
1026
 
1027
+ /** The framework's headers plus the caller's (see callerHeadersOf). */
1028
+ function requestHeaders(options: AvCallOptions): Record<string, string> {
1029
+ return { ...FRAMEWORK_HEADERS, ...callerHeadersOf(options) };
1030
+ }
1031
+
683
1032
  /**
684
- * The framework's headers plus the caller's, refusing a caller's that names one of
685
- * the framework's; then requestId, which overwrites any request id header the
686
- * caller set.
1033
+ * The caller's headers, refusing one that names one of the framework's; then
1034
+ * requestId, which overwrites any request id header the caller set.
687
1035
  */
688
- function requestHeaders(options: AvCallOptions): Record<string, string> {
689
- const headers: Record<string, string> = { ...FRAMEWORK_HEADERS };
1036
+ function callerHeadersOf(options: AvCallOptions): Record<string, string> {
1037
+ const headers: Record<string, string> = {};
690
1038
  const owned = new Set(Object.keys(FRAMEWORK_HEADERS).map((name) => name.toLowerCase()));
691
1039
  for (const [name, value] of Object.entries(options.headers ?? {})) {
692
1040
  if (owned.has(name.toLowerCase())) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aventara/client",
3
- "version": "1.0.0-rc.10",
3
+ "version": "1.0.0-rc.12",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "Development-time generator for Aventara typed remote clients.",
6
6
  "type": "module",
@@ -28,7 +28,7 @@
28
28
  "CHANGELOG.md"
29
29
  ],
30
30
  "dependencies": {
31
- "@aventara/core": "1.0.0-rc.10",
31
+ "@aventara/core": "1.0.0-rc.12",
32
32
  "c12": "3.3.4",
33
33
  "get-tsconfig": "4.10.0"
34
34
  },
@@ -44,7 +44,7 @@
44
44
  "access": "public"
45
45
  },
46
46
  "devDependencies": {
47
- "@aventara/testing": "1.0.0-rc.10",
47
+ "@aventara/testing": "1.0.0-rc.12",
48
48
  "@types/node": "24.10.1",
49
49
  "typescript": "^5.9.2"
50
50
  },