@sanoma-ai/workflows 0.9.1 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +193 -50
- package/SECRET-SUPPORT.md +33 -0
- package/dist/apply-steps.js +5 -2
- package/dist/apply-steps.js.map +1 -1
- package/dist/apply.d.ts +3 -0
- package/dist/apply.js +27 -13
- package/dist/apply.js.map +1 -1
- package/dist/approvals.d.ts +1 -0
- package/dist/approvals.js +6 -1
- package/dist/approvals.js.map +1 -1
- package/dist/apps.d.ts +13 -24
- package/dist/apps.js +69 -68
- package/dist/apps.js.map +1 -1
- package/dist/ast.d.ts +9 -7
- package/dist/ast.js +2 -0
- package/dist/ast.js.map +1 -1
- package/dist/authentication.d.ts +54 -0
- package/dist/authentication.js +138 -0
- package/dist/authentication.js.map +1 -0
- package/dist/call-workflow.d.ts +5 -0
- package/dist/call-workflow.js +4 -3
- package/dist/call-workflow.js.map +1 -1
- package/dist/call.d.ts +3 -2
- package/dist/call.js +36 -7
- package/dist/call.js.map +1 -1
- package/dist/cedar.js +14 -1
- package/dist/cedar.js.map +1 -1
- package/dist/change-steps.js +7 -5
- package/dist/change-steps.js.map +1 -1
- package/dist/change.d.ts +7 -0
- package/dist/change.js +7 -0
- package/dist/change.js.map +1 -1
- package/dist/client.d.ts +62 -10
- package/dist/client.js +272 -47
- package/dist/client.js.map +1 -1
- package/dist/collection.d.ts +7 -0
- package/dist/collection.js +5 -2
- package/dist/collection.js.map +1 -1
- package/dist/config.d.ts +16 -4
- package/dist/config.js +89 -21
- package/dist/config.js.map +1 -1
- package/dist/connection.d.ts +389 -0
- package/dist/connection.js +657 -0
- package/dist/connection.js.map +1 -0
- package/dist/credentials.d.ts +7 -53
- package/dist/credentials.js +84 -247
- package/dist/credentials.js.map +1 -1
- package/dist/db.js +34 -2
- package/dist/db.js.map +1 -1
- package/dist/define.d.ts +9 -2
- package/dist/define.js.map +1 -1
- package/dist/definitions.d.ts +20 -0
- package/dist/definitions.js +16 -0
- package/dist/definitions.js.map +1 -0
- package/dist/deployment-key.d.ts +2 -0
- package/dist/deployment-key.js +22 -0
- package/dist/deployment-key.js.map +1 -0
- package/dist/describe.d.ts +15 -175
- package/dist/describe.js +122 -35
- package/dist/describe.js.map +1 -1
- package/dist/description.d.ts +532 -0
- package/dist/description.js +381 -0
- package/dist/description.js.map +1 -0
- package/dist/direct.js +7 -0
- package/dist/direct.js.map +1 -1
- package/dist/drift.d.ts +1 -1
- package/dist/drift.js +1 -1
- package/dist/drift.js.map +1 -1
- package/dist/env.d.ts +2 -0
- package/dist/env.js +2 -2
- package/dist/env.js.map +1 -1
- package/dist/fake.d.ts +13 -1
- package/dist/fake.js +70 -0
- package/dist/fake.js.map +1 -1
- package/dist/index.d.ts +11 -3
- package/dist/index.js +10 -2
- package/dist/index.js.map +1 -1
- package/dist/ledger.d.ts +2 -0
- package/dist/ledger.js +1 -1
- package/dist/ledger.js.map +1 -1
- package/dist/lint.d.ts +0 -9
- package/dist/lint.js +2 -17
- package/dist/lint.js.map +1 -1
- package/dist/load.d.ts +85 -0
- package/dist/load.js +391 -0
- package/dist/load.js.map +1 -0
- package/dist/lockfile.d.ts +2 -0
- package/dist/lockfile.js +8 -3
- package/dist/lockfile.js.map +1 -1
- package/dist/op.d.ts +22 -5
- package/dist/op.js +20 -2
- package/dist/op.js.map +1 -1
- package/dist/outline.d.ts +5 -0
- package/dist/outline.js +46 -5
- package/dist/outline.js.map +1 -1
- package/dist/paths.d.ts +18 -2
- package/dist/paths.js +19 -7
- package/dist/paths.js.map +1 -1
- package/dist/plugin.d.ts +0 -11
- package/dist/plugin.js +3 -18
- package/dist/plugin.js.map +1 -1
- package/dist/policy.d.ts +2 -0
- package/dist/policy.js.map +1 -1
- package/dist/resource.d.ts +10 -4
- package/dist/resource.js +11 -4
- package/dist/resource.js.map +1 -1
- package/dist/resources.d.ts +33 -76
- package/dist/resources.js +219 -227
- package/dist/resources.js.map +1 -1
- package/dist/run.d.ts +4 -0
- package/dist/run.js.map +1 -1
- package/dist/sandbox.d.ts +5 -2
- package/dist/sandbox.js +56 -7
- package/dist/sandbox.js.map +1 -1
- package/dist/scenario.d.ts +5 -0
- package/dist/scenario.js +3 -2
- package/dist/scenario.js.map +1 -1
- package/dist/scenarios.js +7 -1
- package/dist/scenarios.js.map +1 -1
- package/dist/schema.js +8 -0
- package/dist/schema.js.map +1 -1
- package/dist/secret-store-check.d.ts +11 -0
- package/dist/secret-store-check.js +76 -0
- package/dist/secret-store-check.js.map +1 -0
- package/dist/secret-store.d.ts +106 -0
- package/dist/secret-store.js +228 -0
- package/dist/secret-store.js.map +1 -0
- package/dist/secret.d.ts +2 -0
- package/dist/secret.js +32 -0
- package/dist/secret.js.map +1 -1
- package/dist/shared.d.ts +21 -11
- package/dist/shared.js +41 -7
- package/dist/shared.js.map +1 -1
- package/dist/sites.d.ts +4 -93
- package/dist/sites.js +76 -84
- package/dist/sites.js.map +1 -1
- package/dist/steps.d.ts +3 -0
- package/dist/steps.js.map +1 -1
- package/dist/version.d.ts +2 -1
- package/dist/version.js +2 -1
- package/dist/version.js.map +1 -1
- package/dist/wakeup-driver.js +15 -6
- package/dist/wakeup-driver.js.map +1 -1
- package/dist/worker.js +12 -8
- package/dist/worker.js.map +1 -1
- package/dist/writer.d.ts +53 -14
- package/dist/writer.js +113 -43
- package/dist/writer.js.map +1 -1
- package/oxlint.json +1 -5
- package/package.json +6 -1
- package/src/apply-steps.ts +5 -2
- package/src/apply.ts +54 -21
- package/src/approvals.ts +7 -1
- package/src/apps.ts +71 -86
- package/src/ast.ts +6 -6
- package/src/authentication.ts +191 -0
- package/src/call-workflow.ts +5 -4
- package/src/call.ts +52 -9
- package/src/cedar.ts +14 -1
- package/src/change-steps.ts +7 -4
- package/src/change.ts +10 -0
- package/src/client.ts +346 -45
- package/src/collection.ts +10 -2
- package/src/config.ts +107 -19
- package/src/connection.ts +912 -0
- package/src/credentials.ts +94 -281
- package/src/db.ts +34 -2
- package/src/define.ts +7 -2
- package/src/definitions.ts +31 -0
- package/src/deployment-key.ts +23 -0
- package/src/describe.ts +150 -227
- package/src/description.ts +426 -0
- package/src/direct.ts +7 -0
- package/src/drift.ts +10 -2
- package/src/env.ts +4 -2
- package/src/fake.ts +71 -0
- package/src/index.ts +53 -1
- package/src/ledger.ts +3 -1
- package/src/lint.ts +2 -17
- package/src/load.ts +466 -0
- package/src/lockfile.ts +10 -3
- package/src/op.ts +41 -7
- package/src/outline.ts +53 -7
- package/src/paths.ts +21 -6
- package/src/plugin.ts +3 -24
- package/src/policy.ts +2 -0
- package/src/resource.ts +24 -6
- package/src/resources.ts +246 -301
- package/src/run.ts +4 -0
- package/src/sandbox.ts +61 -7
- package/src/scenario.ts +24 -6
- package/src/scenarios.ts +7 -1
- package/src/schema.ts +10 -0
- package/src/secret-store-check.ts +93 -0
- package/src/secret-store.ts +314 -0
- package/src/secret.ts +29 -0
- package/src/shared.ts +44 -10
- package/src/sites.ts +85 -179
- package/src/steps.ts +3 -2
- package/src/version.ts +2 -1
- package/src/wakeup-driver.ts +22 -6
- package/src/worker.ts +12 -8
- package/src/writer.ts +145 -56
- package/dist/datafile.d.ts +0 -97
- package/dist/datafile.js +0 -479
- package/dist/datafile.js.map +0 -1
- package/src/datafile.ts +0 -601
package/src/config.ts
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
import { configuredSecrets } from "./credentials.ts";
|
|
2
|
+
import { secretsForInput } from "./secret.ts";
|
|
3
|
+
import { isRunControlError } from "./errors.ts";
|
|
4
|
+
import { currentRun } from "./run.ts";
|
|
5
|
+
import {
|
|
6
|
+
connectionCatalog,
|
|
7
|
+
ConnectionRegistry,
|
|
8
|
+
connectionDriver,
|
|
9
|
+
connectionOps,
|
|
10
|
+
type ConnectionDefinition,
|
|
11
|
+
type ConnectionRule,
|
|
12
|
+
} from "./connection.ts";
|
|
13
|
+
import { checkSecretConfiguration, type SecretConfiguration } from "./secret-store.ts";
|
|
1
14
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
15
|
import { dirname, join, resolve } from "node:path";
|
|
3
16
|
import { pathToFileURL } from "node:url";
|
|
@@ -81,6 +94,10 @@ export interface SanomaConfig {
|
|
|
81
94
|
* only, for tests.
|
|
82
95
|
*/
|
|
83
96
|
ledger?: LedgerStore;
|
|
97
|
+
/** Credential storage and explicit bindings. Omitted: encrypted Postgres, no environment fallback. */
|
|
98
|
+
secrets?: SecretConfiguration;
|
|
99
|
+
connections?: readonly ConnectionDefinition[];
|
|
100
|
+
connectionRules?: readonly ConnectionRule[];
|
|
84
101
|
/** Postgres for the runtime. Defaults to `SANOMA_DATABASE_URL`, then `DEFAULT_DATABASE_URL`. */
|
|
85
102
|
databaseUrl?: string;
|
|
86
103
|
/** The file that defined the config, absolute, when known. Set by `defineConfig` from its call site. */
|
|
@@ -118,6 +135,9 @@ export interface SanomaConfig {
|
|
|
118
135
|
|
|
119
136
|
/** A config, checked, with everything a worker, client or app derives from it. */
|
|
120
137
|
export interface ResolvedConfig {
|
|
138
|
+
secrets: SecretConfiguration;
|
|
139
|
+
connections: ConnectionRegistry;
|
|
140
|
+
connectionPool: LazyPool;
|
|
121
141
|
/**
|
|
122
142
|
* Scopes workflows, queues and versions in the system database: the `name` of
|
|
123
143
|
* `<root>/package.json` when it names one, else "sanoma".
|
|
@@ -252,6 +272,7 @@ export function rootOf(config: Pick<SanomaConfig, "root" | "file">): string | un
|
|
|
252
272
|
* `startApp`, the `sanoma` bin when it loads the config.
|
|
253
273
|
*/
|
|
254
274
|
export function resolveConfig(config: SanomaConfig): ResolvedConfig {
|
|
275
|
+
checkSecretConfiguration(config.secrets ?? {});
|
|
255
276
|
if (!Array.isArray(config.connectors)) {
|
|
256
277
|
throw new Error("The config needs `connectors`: the connectors its workflows call, as their packages export them");
|
|
257
278
|
}
|
|
@@ -280,6 +301,12 @@ export function resolveConfig(config: SanomaConfig): ResolvedConfig {
|
|
|
280
301
|
const hostTyped = [...types.values()].some((type) => type.host);
|
|
281
302
|
// Built-in: the collections' operations, `data.<name>.<op>`, on the config's database.
|
|
282
303
|
const databaseUrl = resolveDatabaseUrl(config);
|
|
304
|
+
const connectionPool = lazyPool(databaseUrl, "connections");
|
|
305
|
+
const connections = new ConnectionRegistry(
|
|
306
|
+
connectionCatalog(config.connectors, config.connections, config.connectionRules),
|
|
307
|
+
() => connectionPool.db(),
|
|
308
|
+
configuredSecrets(config.secrets ?? {}, () => connectionPool.db()),
|
|
309
|
+
);
|
|
283
310
|
const declared = [...collections.values()];
|
|
284
311
|
const dataStore = declared.length ? lazyPool(databaseUrl, "data") : undefined;
|
|
285
312
|
// Built-in, in every config: a change's operations, which the template's policy names
|
|
@@ -287,6 +314,7 @@ export function resolveConfig(config: SanomaConfig): ResolvedConfig {
|
|
|
287
314
|
const ops = indexConnectors([
|
|
288
315
|
...running,
|
|
289
316
|
changeOps,
|
|
317
|
+
connectionOps,
|
|
290
318
|
appsListOps,
|
|
291
319
|
...(hostTyped ? [sourceOps] : []),
|
|
292
320
|
...(dataStore ? [collectionsConnector(declared)] : []),
|
|
@@ -299,11 +327,13 @@ export function resolveConfig(config: SanomaConfig): ResolvedConfig {
|
|
|
299
327
|
[
|
|
300
328
|
...driversByVendor.values(),
|
|
301
329
|
changeDriver(remotes),
|
|
330
|
+
connectionDriver(connections),
|
|
302
331
|
appsListDriver,
|
|
303
332
|
...(hostTyped ? [sourceDriver] : []),
|
|
304
333
|
...(dataStore ? [collectionsDriver(declared, dataStore)] : []),
|
|
305
334
|
],
|
|
306
335
|
ops,
|
|
336
|
+
{ connections },
|
|
307
337
|
);
|
|
308
338
|
const fakes = makersOf(running, "fake");
|
|
309
339
|
if (dataStore) fakes.set(DATA_VENDOR, () => collectionsFake(declared));
|
|
@@ -326,7 +356,7 @@ export function resolveConfig(config: SanomaConfig): ResolvedConfig {
|
|
|
326
356
|
const connectors = connectorsOf(config);
|
|
327
357
|
// The types the data files may declare: the vendors' and the people's, whose resources are left out.
|
|
328
358
|
const declarable = resourceTypesOf(connectors);
|
|
329
|
-
const read = () => readDeclared(root, connectors, declarable, types);
|
|
359
|
+
const read = () => readDeclared(root, connectors, declarable, types, connections);
|
|
330
360
|
if (types.size) {
|
|
331
361
|
names.set(
|
|
332
362
|
DRIFT_WORKFLOW,
|
|
@@ -343,6 +373,9 @@ export function resolveConfig(config: SanomaConfig): ResolvedConfig {
|
|
|
343
373
|
if (call.uses.length) names.set(CALL_WORKFLOW, call);
|
|
344
374
|
const { policy, policies, problems: policyProblems } = policyAt(root, { ops, workflows: names, collections });
|
|
345
375
|
return {
|
|
376
|
+
secrets: config.secrets ?? {},
|
|
377
|
+
connections,
|
|
378
|
+
connectionPool,
|
|
346
379
|
appName,
|
|
347
380
|
databaseUrl,
|
|
348
381
|
// The built-in's code and operations are part of the app's version, like the config's own.
|
|
@@ -495,9 +528,9 @@ export async function declaredWakeup(config: SanomaConfig): Promise<DeclaredWake
|
|
|
495
528
|
const connectors = connectorsOf(config);
|
|
496
529
|
const types = resourceTypesOf(connectors);
|
|
497
530
|
if (![...types.values()].some((type) => type.wakeup)) return undefined;
|
|
498
|
-
// Loaded when it is needed, not with the worker
|
|
499
|
-
const {
|
|
500
|
-
const { resources } =
|
|
531
|
+
// Loaded when it is needed, not with the worker.
|
|
532
|
+
const { loadDataFiles } = await import("./resources.ts");
|
|
533
|
+
const { resources } = loadDataFiles(rootOf(config), connectors, types);
|
|
501
534
|
const typeOf = (r: DeclaredResource) => types.get(`${r.vendor}.${r.type}`)!;
|
|
502
535
|
const declared = resources.find((r) => typeOf(r).wakeup);
|
|
503
536
|
if (!declared) return undefined;
|
|
@@ -523,15 +556,38 @@ async function readDeclared(
|
|
|
523
556
|
connectors: readonly Connector<any, any>[],
|
|
524
557
|
declarable: ReadonlyMap<string, Resource>,
|
|
525
558
|
types: ReadonlyMap<string, Resource>,
|
|
559
|
+
connections: ConnectionRegistry,
|
|
526
560
|
): Promise<Omit<ApplyDeclared, "rebinds">> {
|
|
527
|
-
// Loaded when a run reads the files, not with the worker
|
|
528
|
-
const {
|
|
529
|
-
const { resources, bindings, orphans, problems } =
|
|
561
|
+
// Loaded when a run reads the files, not with the worker.
|
|
562
|
+
const { loadDataFiles } = await import("./resources.ts");
|
|
563
|
+
const { resources, bindings, orphans, problems } = loadDataFiles(root, connectors, declarable);
|
|
564
|
+
// Read inside the data-file durable step: every operation of the plan uses one pinned account.
|
|
565
|
+
const run = currentRun.getStore();
|
|
566
|
+
if (run && !run.sandbox) {
|
|
567
|
+
for (const resource of resources.filter((r) => types.has(`${r.vendor}.${r.type}`))) {
|
|
568
|
+
try {
|
|
569
|
+
const selected = await connections.select(resource.vendor, resource.connection, run.actor.id);
|
|
570
|
+
if (selected) {
|
|
571
|
+
const pinned = { vendor: selected.vendor, $connection: selected.id, revision: selected.revision };
|
|
572
|
+
const locked = bindings[resource.id]?.connection;
|
|
573
|
+
if (locked && (locked.$connection !== pinned.$connection || locked.revision !== pinned.revision))
|
|
574
|
+
throw new Error("The bound resource's account changed; explicitly unbind before using a new account");
|
|
575
|
+
resource.connection = pinned;
|
|
576
|
+
}
|
|
577
|
+
} catch {
|
|
578
|
+
problems.push({
|
|
579
|
+
file: resource.file,
|
|
580
|
+
message: `${resource.id}: its connection is unavailable or changed; reconnect and explicitly rebind`,
|
|
581
|
+
});
|
|
582
|
+
resources.splice(resources.indexOf(resource), 1);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
530
586
|
return {
|
|
531
587
|
// Only `types`' resources have a vendor object to check or apply: not the people's (vendorless).
|
|
532
588
|
resources: resources
|
|
533
589
|
.filter((r) => types.has(`${r.vendor}.${r.type}`))
|
|
534
|
-
.map(({ id, vendor, type, name, identity, binding, desired, refs }) => ({
|
|
590
|
+
.map(({ id, vendor, type, name, identity, binding, desired, refs, connection }) => ({
|
|
535
591
|
id,
|
|
536
592
|
vendor,
|
|
537
593
|
type,
|
|
@@ -540,6 +596,7 @@ async function readDeclared(
|
|
|
540
596
|
...(binding !== undefined && { binding }),
|
|
541
597
|
desired: withoutWriteOnly(types.get(`${vendor}.${type}`)!.fields, desired),
|
|
542
598
|
refs,
|
|
599
|
+
...(connection && { connection }),
|
|
543
600
|
})),
|
|
544
601
|
bindings,
|
|
545
602
|
orphans,
|
|
@@ -563,11 +620,15 @@ function indexConnectors(list: Connector<any, any>[]) {
|
|
|
563
620
|
}
|
|
564
621
|
|
|
565
622
|
/**
|
|
566
|
-
* The drivers' functions by operation id, refusing one no connector declares.
|
|
567
|
-
*
|
|
568
|
-
*
|
|
623
|
+
* The drivers' functions by operation id, refusing one no connector declares. Named live
|
|
624
|
+
* calls resolve authentication here, after policy, inside the driver's execution boundary.
|
|
625
|
+
* Drivers without named authentication retain their declared environment guard.
|
|
569
626
|
*/
|
|
570
|
-
export function indexDrivers(
|
|
627
|
+
export function indexDrivers(
|
|
628
|
+
list: Driver[],
|
|
629
|
+
ops: Map<string, Op>,
|
|
630
|
+
options: { connections?: ConnectionRegistry; sandbox?: boolean } = {},
|
|
631
|
+
) {
|
|
571
632
|
const map = new Map<string, DriverFn>();
|
|
572
633
|
for (const d of list) {
|
|
573
634
|
for (const [key, fn] of Object.entries(d.ops)) {
|
|
@@ -575,7 +636,36 @@ export function indexDrivers(list: Driver[], ops: Map<string, Op>) {
|
|
|
575
636
|
if (!ops.has(id)) {
|
|
576
637
|
throw new Error(`Driver "${d.vendor}" implements ${id}, which no connector in \`connectors\` declares`);
|
|
577
638
|
}
|
|
578
|
-
|
|
639
|
+
const unselected = d.env ? guarded(d.vendor, d.env, fn) : fn;
|
|
640
|
+
map.set(id, async (input, call) => {
|
|
641
|
+
if (options.sandbox) return fn(input, call);
|
|
642
|
+
if (!call.connection) {
|
|
643
|
+
if (d.authentication?.length)
|
|
644
|
+
throw new DriverError("Select a named connection for this driver", { retryable: false });
|
|
645
|
+
return unselected(input, call);
|
|
646
|
+
}
|
|
647
|
+
if (!options.connections || !call.principal || !d.authentication?.includes(call.connection.method))
|
|
648
|
+
throw new DriverError("This driver does not support the selected connection", { retryable: false });
|
|
649
|
+
const authentication = await options.connections.authenticate(call.connection, call.principal.id);
|
|
650
|
+
try {
|
|
651
|
+
return await fn(input, {
|
|
652
|
+
...call,
|
|
653
|
+
connection: structuredClone(call.connection),
|
|
654
|
+
authentication,
|
|
655
|
+
secrets: secretsForInput(input, call.secrets, ops.get(id)!.createsSecrets?.(input)),
|
|
656
|
+
});
|
|
657
|
+
} catch (error) {
|
|
658
|
+
if (isRunControlError(error)) throw error;
|
|
659
|
+
// SDK exceptions can contain the complete request, including authorization headers.
|
|
660
|
+
// Drivers may come from another installed copy of this package.
|
|
661
|
+
const failure = error as { code?: unknown; retryable?: unknown; status?: unknown } | null;
|
|
662
|
+
const status = failure?.code === "driver_failed" ? failure.status : undefined;
|
|
663
|
+
throw new DriverError(`${d.vendor} operation failed`, {
|
|
664
|
+
retryable: failure?.code === "driver_failed" && failure.retryable === true,
|
|
665
|
+
...(typeof status === "number" && Number.isInteger(status) && status >= 100 && status <= 599 && { status }),
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
});
|
|
579
669
|
}
|
|
580
670
|
}
|
|
581
671
|
return map;
|
|
@@ -583,12 +673,10 @@ export function indexDrivers(list: Driver[], ops: Map<string, Op>) {
|
|
|
583
673
|
|
|
584
674
|
/**
|
|
585
675
|
* `fn`, refused while the environment leaves a variable `env` declares missing or invalid. The
|
|
586
|
-
* worker started without it (`startWorker` only warns)
|
|
587
|
-
* environment
|
|
588
|
-
* driver step
|
|
589
|
-
*
|
|
590
|
-
* used when the approver approves. The refusal is the call's own failure, final: recorded as
|
|
591
|
-
* the call's, and never retried.
|
|
676
|
+
* worker started without it (`startWorker` only warns). This checks only explicitly supplied
|
|
677
|
+
* process environment; stored credentials are never copied there. The guard runs inside the
|
|
678
|
+
* driver step after policy and approval. Named connections never use this path. The refusal
|
|
679
|
+
* is the call's own failure, final: recorded as the call's, and never retried.
|
|
592
680
|
*/
|
|
593
681
|
function guarded(vendor: string, env: DriverEnv, fn: DriverFn): DriverFn {
|
|
594
682
|
return async (input, call) => {
|