@zapier/zapier-sdk-cli 0.60.0 → 0.61.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/dist/cli.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command, CommanderError, Option } from 'commander';
3
3
  import { z } from 'zod';
4
- import { definePlugin, createPluginMethod, OutputPropertySchema, ZapierBundleError, DEFAULT_CONFIG_PATH, ZapierValidationError, ZapierUnknownError, ZapierReleaseTriggerMessageSignal, injectCliLogin, BaseSdkOptionsSchema, getOrCreateApiClient, isPermanentHttpError, invalidateCachedToken, batch, toSnakeCase, ZapierAbortDrainSignal, createZapierSdkStack as createZapierSdkStack$1, createSdk as createSdk$1, defineLegacyMerge as defineLegacyMerge$1, zapierSdkPlugin as zapierSdkPlugin$1, addPlugin as addPlugin$1, ZapierError, isCredentialsObject, buildApplicationLifecycleEvent, AuthMechanism, ZapierAuthenticationError, DEPRECATION_NOTICE_EVENT, runWithCallerContext, isPositional, runWithTelemetryContext, buildCapabilityMessage, formatErrorMessage, getOsInfo, getPlatformVersions, getAgent, getTtyContext, getCiPlatform, isCi, getReleaseId, getCurrentTimestamp, generateEventId, ZapierApprovalError } from '@zapier/zapier-sdk';
4
+ import { definePlugin, createPluginMethod, OutputPropertySchema, ZapierBundleError, DEFAULT_CONFIG_PATH, ZapierValidationError, ZapierUnknownError, ZapierReleaseTriggerMessageSignal, injectCliLogin, BaseSdkOptionsSchema, getOrCreateApiClient, isPermanentHttpError, invalidateCachedToken, batch, toSnakeCase, ZapierAbortDrainSignal, createZapierSdkStack as createZapierSdkStack$1, createSdk as createSdk$1, defineLegacyMerge as defineLegacyMerge$1, zapierSdkPlugin as zapierSdkPlugin$1, addPlugin as addPlugin$1, ZapierError, isCredentialsObject, buildApplicationLifecycleEvent, AuthMechanism, ZapierAuthenticationError, DEPRECATION_NOTICE_EVENT, runWithCallerContext, isPositional, createController, CoreCancelledSignal, runWithTelemetryContext, buildCapabilityMessage, formatErrorMessage, getOsInfo, getPlatformVersions, getAgent, getTtyContext, getCiPlatform, isCi, getReleaseId, getCurrentTimestamp, generateEventId, ZapierApprovalError } from '@zapier/zapier-sdk';
5
5
  import inquirer from 'inquirer';
6
6
  import search from '@inquirer/search';
7
7
  import chalk from 'chalk';
@@ -59,12 +59,18 @@ var ZapierCliExitError = class extends ZapierCliError {
59
59
  this.exitCode = exitCode;
60
60
  }
61
61
  };
62
- var ZapierCliValidationError = class extends ZapierCliError {
63
- constructor(message) {
62
+ var ZapierCliValidationError = class _ZapierCliValidationError extends ZapierCliError {
63
+ constructor(message, options = {}) {
64
64
  super(message);
65
- this.name = "ZapierCliValidationError";
66
- this.code = "ZAPIER_CLI_VALIDATION_ERROR";
67
65
  this.exitCode = 1;
66
+ this.name = options.name ?? "ZapierCliValidationError";
67
+ this.code = options.code ?? "ZAPIER_CLI_VALIDATION_ERROR";
68
+ }
69
+ withMessage(message) {
70
+ return new _ZapierCliValidationError(message, {
71
+ name: this.name,
72
+ code: this.code
73
+ });
68
74
  }
69
75
  };
70
76
  var ZapierCliMissingParametersError = class extends ZapierCliError {
@@ -1505,6 +1511,148 @@ Optional fields${pathContext}:`));
1505
1511
  return constants;
1506
1512
  }
1507
1513
  };
1514
+ function offers(question, action) {
1515
+ return question.actions.some((a) => a.action === action);
1516
+ }
1517
+ function isActionRow(value) {
1518
+ return typeof value === "object" && value !== null && "action" in value;
1519
+ }
1520
+ async function promptText({
1521
+ message,
1522
+ password
1523
+ }) {
1524
+ const { value } = await inquirer.prompt([
1525
+ { type: password ? "password" : "input", name: "value", message }
1526
+ ]);
1527
+ return value;
1528
+ }
1529
+ async function answerSelect(question, field) {
1530
+ const display = (c) => c.hint ? `${c.label} ${chalk.dim(`(${c.hint})`)}` : c.label;
1531
+ if (question.multiple) {
1532
+ const choices = [
1533
+ ...question.choices.map((c) => ({ name: display(c), value: c.value })),
1534
+ ...offers(question, "more") ? [
1535
+ {
1536
+ name: chalk.dim("Load more\u2026"),
1537
+ value: { action: "more" }
1538
+ }
1539
+ ] : [],
1540
+ ...(question.notes ?? []).map((note) => ({
1541
+ name: chalk.dim(note),
1542
+ value: note,
1543
+ disabled: true
1544
+ }))
1545
+ ];
1546
+ const { values } = await inquirer.prompt([
1547
+ { type: "checkbox", name: "values", message: question.message, choices }
1548
+ ]);
1549
+ const selected = values;
1550
+ if (selected.some(isActionRow)) return { type: "more" };
1551
+ if (selected.length === 0 && offers(question, "skip")) {
1552
+ return { type: "skip" };
1553
+ }
1554
+ return { type: "choose", value: selected };
1555
+ }
1556
+ const row = (name, action) => ({
1557
+ name,
1558
+ value: { action }
1559
+ });
1560
+ const value = await search({
1561
+ message: question.message,
1562
+ source: (term) => {
1563
+ const t = (term ?? "").trim().toLowerCase();
1564
+ const matched = t ? question.choices.filter(
1565
+ (c) => `${c.label} ${c.hint ?? ""}`.toLowerCase().includes(t)
1566
+ ) : question.choices;
1567
+ const rows = matched.map((c) => ({
1568
+ name: display(c),
1569
+ value: c.value
1570
+ }));
1571
+ if (offers(question, "search"))
1572
+ rows.push(row(chalk.cyan("Search\u2026"), "search"));
1573
+ if (offers(question, "more"))
1574
+ rows.push(row(chalk.dim("Load more\u2026"), "more"));
1575
+ if (offers(question, "custom"))
1576
+ rows.push(row(chalk.dim("Enter a value manually\u2026"), "custom"));
1577
+ if (offers(question, "retry"))
1578
+ rows.push(row(chalk.yellow("Retry"), "retry"));
1579
+ if (offers(question, "skip"))
1580
+ rows.push(row(chalk.dim("Skip (optional)"), "skip"));
1581
+ if (offers(question, "cancel"))
1582
+ rows.push(row(chalk.dim("Cancel"), "cancel"));
1583
+ for (const note of question.notes ?? [])
1584
+ rows.push({ name: chalk.dim(note), value: note, disabled: true });
1585
+ return rows;
1586
+ }
1587
+ });
1588
+ if (!isActionRow(value)) {
1589
+ return { type: "choose", value };
1590
+ }
1591
+ switch (value.action) {
1592
+ case "search": {
1593
+ const term = await promptText({
1594
+ message: `Search ${field}:`,
1595
+ password: false
1596
+ });
1597
+ return { type: "search", term };
1598
+ }
1599
+ case "custom": {
1600
+ const custom = await promptText({
1601
+ message: `Enter ${field}:`,
1602
+ password: false
1603
+ });
1604
+ return { type: "custom", value: custom };
1605
+ }
1606
+ case "more":
1607
+ return { type: "more" };
1608
+ case "retry":
1609
+ return { type: "retry" };
1610
+ case "skip":
1611
+ return { type: "skip" };
1612
+ case "cancel":
1613
+ return { type: "cancel" };
1614
+ }
1615
+ }
1616
+ async function answerInput(question) {
1617
+ const value = await promptText({
1618
+ message: question.message,
1619
+ password: question.inputType === "password"
1620
+ });
1621
+ if (value === "" && offers(question, "skip")) {
1622
+ return { type: "skip" };
1623
+ }
1624
+ return { type: "custom", value };
1625
+ }
1626
+ async function answerCollection(question) {
1627
+ if (!offers(question, "done")) {
1628
+ return { type: "add" };
1629
+ }
1630
+ const { again } = await inquirer.prompt([
1631
+ {
1632
+ type: "confirm",
1633
+ name: "again",
1634
+ message: question.message,
1635
+ default: false
1636
+ }
1637
+ ]);
1638
+ return again ? { type: "add" } : { type: "done" };
1639
+ }
1640
+ var answerViaCli = ({ state, result }) => {
1641
+ if (result.error !== void 0) {
1642
+ const message = typeof result.error === "string" ? result.error : result.error.message;
1643
+ console.log(chalk.yellow(`! ${message}`));
1644
+ }
1645
+ const field = state.current?.join(".") ?? "value";
1646
+ const question = result.question;
1647
+ switch (question.type) {
1648
+ case "select":
1649
+ return answerSelect(question, field);
1650
+ case "input":
1651
+ return answerInput(question);
1652
+ case "collection":
1653
+ return answerCollection(question);
1654
+ }
1655
+ };
1508
1656
 
1509
1657
  // src/utils/cli-options.ts
1510
1658
  var RESERVED_CLI_OPTIONS = [
@@ -1536,7 +1684,7 @@ var SHARED_COMMAND_CLI_OPTIONS = [
1536
1684
 
1537
1685
  // package.json
1538
1686
  var package_default = {
1539
- version: "0.60.0"};
1687
+ version: "0.61.1"};
1540
1688
 
1541
1689
  // src/telemetry/builders.ts
1542
1690
  function createCliBaseEvent(context = {}) {
@@ -1611,7 +1759,7 @@ async function formatItemsFromSchema(_functionInfo, items, startingNumber = 0, o
1611
1759
  if (options?.formatter) {
1612
1760
  const formatter = options.formatter;
1613
1761
  const input = options.input ?? {};
1614
- const context = formatter.fetchContext ? await formatter.fetchContext({ items, input }) : void 0;
1762
+ const context = formatter.getContext ? await formatter.getContext({ items, input }) : void 0;
1615
1763
  items.forEach((item, index) => {
1616
1764
  const formatted = formatter.format({ item, input, context });
1617
1765
  formatSingleItem(formatted, startingNumber + index);
@@ -2206,7 +2354,7 @@ function analyzeZodField(name, schema, functionInfo) {
2206
2354
  paramType = "object";
2207
2355
  }
2208
2356
  let paramHasResolver = false;
2209
- if (functionInfo?.resolvers?.[name]) {
2357
+ if (functionInfo?.resolvers?.[name] || functionInfo?.boundResolvers?.[name]) {
2210
2358
  paramHasResolver = true;
2211
2359
  }
2212
2360
  return {
@@ -2365,6 +2513,9 @@ function createCommandConfig(cliCommandName, functionInfo, sdk) {
2365
2513
  const usesInputParameters = !functionInfo.inputSchema && !!functionInfo.inputParameters;
2366
2514
  const schema = functionInfo.inputSchema;
2367
2515
  const parameters = usesInputParameters ? analyzeInputParameters(functionInfo.inputParameters, functionInfo) : analyzeZodSchema(schema, functionInfo);
2516
+ if (functionInfo.boundResolvers && Object.keys(functionInfo.boundResolvers).length > 0) {
2517
+ for (const param of parameters) param.hasResolver = true;
2518
+ }
2368
2519
  const schemaAliases = getSchemaAliases(schema);
2369
2520
  if (schemaAliases) {
2370
2521
  const aliasedNames = new Set(Object.keys(schemaAliases));
@@ -2417,7 +2568,35 @@ function createCommandConfig(cliCommandName, functionInfo, sdk) {
2417
2568
  }
2418
2569
  }
2419
2570
  }
2420
- if (schema && !usesInputParameters) {
2571
+ const boundResolvers = functionInfo.boundResolvers;
2572
+ if (boundResolvers && Object.keys(boundResolvers).length > 0) {
2573
+ const seedInput = Object.fromEntries(
2574
+ Object.entries(rawParams).filter(([, v]) => v !== void 0)
2575
+ );
2576
+ const controller = createController(sdk);
2577
+ let resolved;
2578
+ try {
2579
+ resolved = await controller.resolve({
2580
+ method: functionInfo.name,
2581
+ input: seedInput,
2582
+ answer: interactiveMode ? answerViaCli : ({ state }) => {
2583
+ throw new ZapierCliMissingParametersError([
2584
+ {
2585
+ name: state.current?.join(".") ?? "value",
2586
+ isPositional: false
2587
+ }
2588
+ ]);
2589
+ },
2590
+ interactive: interactiveMode
2591
+ });
2592
+ } catch (err) {
2593
+ if (err instanceof CoreCancelledSignal) {
2594
+ throw new ZapierCliUserCancellationError();
2595
+ }
2596
+ throw err;
2597
+ }
2598
+ Object.assign(resolvedParams, resolved);
2599
+ } else if (schema && !usesInputParameters) {
2421
2600
  const resolver = new SchemaParameterResolver();
2422
2601
  const resolved = await resolver.resolveParameters(
2423
2602
  schema,
@@ -2510,7 +2689,7 @@ ${confirmMessageAfter}`));
2510
2689
  }
2511
2690
  }
2512
2691
  } catch (error) {
2513
- success = false;
2692
+ success = error instanceof ZapierCliError ? error.exitCode === 0 : false;
2514
2693
  errorMessage = error instanceof Error ? error.message : String(error);
2515
2694
  if (error instanceof ZapierCliMissingParametersError) {
2516
2695
  renderer.renderError(error);
@@ -3722,41 +3901,165 @@ var spinPromise = async (promise, text) => {
3722
3901
  }
3723
3902
  };
3724
3903
 
3904
+ // src/utils/auth/oauth-errors.ts
3905
+ var OauthFlowTimeoutError = class _OauthFlowTimeoutError extends ZapierCliValidationError {
3906
+ constructor({
3907
+ timeoutMs,
3908
+ message = "OAuth flow timed out"
3909
+ }) {
3910
+ super(message, {
3911
+ name: "OauthFlowTimeoutError",
3912
+ code: "ZAPIER_OAUTH_FLOW_TIMEOUT"
3913
+ });
3914
+ this.timeoutMs = timeoutMs;
3915
+ }
3916
+ withMessage(message) {
3917
+ return new _OauthFlowTimeoutError({ timeoutMs: this.timeoutMs, message });
3918
+ }
3919
+ };
3920
+ var OauthAuthorizationDeniedError = class _OauthAuthorizationDeniedError extends ZapierCliValidationError {
3921
+ constructor({
3922
+ reason,
3923
+ message = "OAuth authorization denied"
3924
+ }) {
3925
+ super(message, {
3926
+ name: "OauthAuthorizationDeniedError",
3927
+ code: "ZAPIER_OAUTH_AUTHORIZATION_DENIED"
3928
+ });
3929
+ this.reason = reason;
3930
+ }
3931
+ withMessage(message) {
3932
+ return new _OauthAuthorizationDeniedError({
3933
+ reason: this.reason,
3934
+ message
3935
+ });
3936
+ }
3937
+ };
3938
+ var OauthFlowError = class _OauthFlowError extends ZapierCliValidationError {
3939
+ constructor({ message }) {
3940
+ super(message, {
3941
+ name: "OauthFlowError",
3942
+ code: "ZAPIER_OAUTH_FLOW"
3943
+ });
3944
+ }
3945
+ withMessage(message) {
3946
+ return new _OauthFlowError({ message });
3947
+ }
3948
+ };
3949
+ var OauthCallbackError = class _OauthCallbackError extends ZapierCliValidationError {
3950
+ constructor({ kind, message }) {
3951
+ super(message, {
3952
+ name: "OauthCallbackError",
3953
+ code: "ZAPIER_OAUTH_CALLBACK"
3954
+ });
3955
+ this.kind = kind;
3956
+ }
3957
+ withMessage(message) {
3958
+ return new _OauthCallbackError({ kind: this.kind, message });
3959
+ }
3960
+ };
3961
+ var OauthTokenExchangeError = class _OauthTokenExchangeError extends ZapierCliValidationError {
3962
+ constructor({ message }) {
3963
+ super(message, {
3964
+ name: "OauthTokenExchangeError",
3965
+ code: "ZAPIER_OAUTH_TOKEN_EXCHANGE"
3966
+ });
3967
+ }
3968
+ withMessage(message) {
3969
+ return new _OauthTokenExchangeError({ message });
3970
+ }
3971
+ };
3972
+ var SENSITIVE_OAUTH_FIELDS = [
3973
+ "access_token",
3974
+ "refresh_token",
3975
+ "id_token",
3976
+ "client_secret",
3977
+ "code_verifier",
3978
+ "code_challenge"
3979
+ ];
3980
+ function getErrorMessage(error) {
3981
+ return error instanceof Error ? error.message : String(error);
3982
+ }
3983
+ function toCamelCase(field) {
3984
+ return field.replace(
3985
+ /_([a-z])/g,
3986
+ (_match, letter) => letter.toUpperCase()
3987
+ );
3988
+ }
3989
+ function escapeRegExp(value) {
3990
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3991
+ }
3992
+ var sensitiveOauthFieldPattern = Array.from(
3993
+ new Set(
3994
+ SENSITIVE_OAUTH_FIELDS.flatMap((field) => [field, toCamelCase(field)])
3995
+ )
3996
+ ).map(escapeRegExp).join("|");
3997
+ var sensitiveQueryParamPattern = new RegExp(
3998
+ `([?&])(${sensitiveOauthFieldPattern})(=)[^&#\\s"'<>]*`,
3999
+ "gi"
4000
+ );
4001
+ function redactSensitiveOauthErrorMessage(message) {
4002
+ return message.replace(
4003
+ sensitiveQueryParamPattern,
4004
+ (_match, prefix, key, separator) => `${prefix}${key}${separator}[REDACTED]`
4005
+ ).replace(
4006
+ new RegExp(`"(${sensitiveOauthFieldPattern})"(\\s*:\\s*)"[^"]*"`, "g"),
4007
+ (_match, key, separator) => `"${key}"${separator}"[REDACTED]"`
4008
+ );
4009
+ }
4010
+ function toRedactedOauthError(error) {
4011
+ const message = redactSensitiveOauthErrorMessage(getErrorMessage(error));
4012
+ if (error instanceof ZapierCliValidationError) {
4013
+ return error.withMessage(message);
4014
+ }
4015
+ return new OauthFlowError({ message });
4016
+ }
4017
+ function toOauthTokenExchangeError(error) {
4018
+ return new OauthTokenExchangeError({
4019
+ message: redactSensitiveOauthErrorMessage(getErrorMessage(error))
4020
+ });
4021
+ }
4022
+
3725
4023
  // src/utils/auth/oauth-callback.ts
3726
4024
  function getCallbackCode({
3727
4025
  callbackUrl,
3728
- transaction,
3729
- recoveryMessage
4026
+ transaction
3730
4027
  }) {
3731
4028
  let parsed;
3732
4029
  try {
3733
4030
  parsed = new URL(callbackUrl.trim());
3734
4031
  } catch {
3735
- throw new ZapierCliValidationError(
3736
- "Paste the final OAuth callback URL from your browser."
3737
- );
4032
+ throw new OauthCallbackError({
4033
+ kind: "invalid_url",
4034
+ message: "Paste the final OAuth callback URL from your browser."
4035
+ });
3738
4036
  }
3739
4037
  const expected = new URL(transaction.redirectUri);
3740
4038
  if (parsed.protocol !== "http:" || parsed.hostname !== expected.hostname || parsed.pathname !== expected.pathname || parsed.port !== expected.port) {
3741
- throw new ZapierCliValidationError(
3742
- `Expected the final OAuth callback URL to start with ${transaction.redirectUri}.`
3743
- );
4039
+ throw new OauthCallbackError({
4040
+ kind: "redirect_mismatch",
4041
+ message: `Expected the final OAuth callback URL to start with ${transaction.redirectUri}.`
4042
+ });
3744
4043
  }
3745
4044
  if (parsed.searchParams.get("state") !== transaction.state) {
3746
- throw new ZapierCliValidationError(
3747
- `OAuth state mismatch.${recoveryMessage ? ` ${recoveryMessage}` : ""}`
3748
- );
4045
+ throw new OauthCallbackError({
4046
+ kind: "state_mismatch",
4047
+ message: "OAuth state mismatch."
4048
+ });
3749
4049
  }
3750
4050
  if (parsed.searchParams.has("error")) {
3751
- throw new ZapierCliValidationError(
3752
- `Authorization denied: ${parsed.searchParams.get("error_description") ?? parsed.searchParams.get("error")}.${recoveryMessage ? ` ${recoveryMessage}` : ""}`
3753
- );
4051
+ throw new OauthAuthorizationDeniedError({
4052
+ reason: String(
4053
+ parsed.searchParams.get("error_description") ?? parsed.searchParams.get("error")
4054
+ )
4055
+ });
3754
4056
  }
3755
4057
  const code = parsed.searchParams.get("code");
3756
4058
  if (!code) {
3757
- throw new ZapierCliValidationError(
3758
- "No authorization code found in the pasted callback URL."
3759
- );
4059
+ throw new OauthCallbackError({
4060
+ kind: "missing_code",
4061
+ message: "No authorization code found in the pasted callback URL."
4062
+ });
3760
4063
  }
3761
4064
  return code;
3762
4065
  }
@@ -3871,7 +4174,9 @@ async function exchangeOauthCode({
3871
4174
  "Content-Type": "application/x-www-form-urlencoded"
3872
4175
  }
3873
4176
  }
3874
- );
4177
+ ).catch((error) => {
4178
+ throw toOauthTokenExchangeError(error);
4179
+ });
3875
4180
  return {
3876
4181
  accessToken: data.access_token,
3877
4182
  refreshToken: data.refresh_token,
@@ -3880,19 +4185,15 @@ async function exchangeOauthCode({
3880
4185
  }
3881
4186
 
3882
4187
  // src/utils/auth/oauth-flow.ts
3883
- var OauthFlowTimeoutError = class extends Error {
3884
- constructor(timeoutMs) {
3885
- super("OAuth flow timed out");
3886
- this.timeoutMs = timeoutMs;
3887
- this.name = "OauthFlowTimeoutError";
3888
- }
4188
+ var LOGIN_OAUTH_COPY = {
4189
+ flowName: "Login",
4190
+ action: "log in",
4191
+ urlLabel: "login URL"
3889
4192
  };
3890
- var OauthAuthorizationDeniedError = class extends Error {
3891
- constructor(reason) {
3892
- super("OAuth authorization denied");
3893
- this.reason = reason;
3894
- this.name = "OauthAuthorizationDeniedError";
3895
- }
4193
+ var SIGNUP_OAUTH_COPY = {
4194
+ flowName: "Signup",
4195
+ action: "sign up",
4196
+ urlLabel: "signup URL"
3896
4197
  };
3897
4198
  function findAvailablePort() {
3898
4199
  return new Promise((resolve4, reject) => {
@@ -3907,70 +4208,86 @@ function findAvailablePort() {
3907
4208
  tryPort(LOGIN_PORTS[portIndex++]);
3908
4209
  } else if (err.code === "EADDRINUSE") {
3909
4210
  reject(
3910
- new Error(
3911
- `All configured OAuth callback ports are busy: ${LOGIN_PORTS.join(", ")}. Please try again later or close applications using these ports.`
3912
- )
4211
+ new OauthFlowError({
4212
+ message: `All configured OAuth callback ports are busy: ${LOGIN_PORTS.join(", ")}. Please try again later or close applications using these ports.`
4213
+ })
3913
4214
  );
3914
4215
  } else {
3915
4216
  reject(err);
3916
4217
  }
3917
4218
  });
3918
4219
  };
3919
- if (LOGIN_PORTS.length > 0) tryPort(LOGIN_PORTS[portIndex++]);
3920
- else reject(new Error("No OAuth callback ports configured"));
4220
+ if (LOGIN_PORTS.length > 0) {
4221
+ tryPort(LOGIN_PORTS[portIndex++]);
4222
+ return;
4223
+ }
4224
+ reject(
4225
+ new OauthFlowError({ message: "No OAuth callback ports configured" })
4226
+ );
3921
4227
  });
3922
4228
  }
3923
4229
  async function runLoginOauthFlow(options) {
3924
- return runOauthFlowEntryPoint({
3925
- ...options,
4230
+ return runOauthFlowForEntryPoint({
4231
+ options,
3926
4232
  entryPoint: "login",
3927
- authAction: "log in",
3928
- flowName: "Login"
4233
+ copy: LOGIN_OAUTH_COPY
3929
4234
  });
3930
4235
  }
3931
4236
  async function runSignupOauthFlow(options) {
4237
+ return runOauthFlowForEntryPoint({
4238
+ options,
4239
+ entryPoint: "signup",
4240
+ copy: SIGNUP_OAUTH_COPY
4241
+ });
4242
+ }
4243
+ function runOauthFlowForEntryPoint({
4244
+ options,
4245
+ entryPoint,
4246
+ copy
4247
+ }) {
3932
4248
  if (options.headless) {
3933
4249
  return runOauthFlowEntryPoint({
3934
4250
  ...options,
3935
- entryPoint: "signup",
3936
- authAction: "sign up",
3937
- flowName: "Signup",
3938
- headless: true
4251
+ entryPoint,
4252
+ headless: true,
4253
+ copy
3939
4254
  });
3940
4255
  }
3941
4256
  return runOauthFlowEntryPoint({
3942
4257
  ...options,
3943
- entryPoint: "signup",
3944
- authAction: "sign up",
3945
- flowName: "Signup"
4258
+ entryPoint,
4259
+ copy,
4260
+ headless: false
3946
4261
  });
3947
4262
  }
3948
- async function runOauthFlowEntryPoint({
3949
- flowName,
3950
- ...options
3951
- }) {
4263
+ async function runOauthFlowEntryPoint(options) {
3952
4264
  try {
3953
- return options.headless ? await runHeadlessSignupOauthFlow(options) : await runOauthFlow(options);
4265
+ return options.headless ? await runHeadlessOauthFlow(options) : await runOauthFlow(options);
3954
4266
  } catch (error) {
3955
4267
  if (error instanceof OauthFlowTimeoutError) {
3956
- throw new Error(
4268
+ throw error.withMessage(
3957
4269
  withRecoveryMessage(
3958
- `${flowName} timed out after ${Math.round(error.timeoutMs / 1e3)} seconds.`,
4270
+ `${options.copy.flowName} timed out after ${Math.round(error.timeoutMs / 1e3)} seconds.`,
3959
4271
  options.recoveryMessage
3960
4272
  )
3961
4273
  );
3962
4274
  }
3963
4275
  if (error instanceof OauthAuthorizationDeniedError) {
3964
- throw new Error(
4276
+ throw error.withMessage(
3965
4277
  withRecoveryMessage(
3966
4278
  `Authorization denied: ${error.reason}.`,
3967
4279
  options.recoveryMessage
3968
4280
  )
3969
4281
  );
3970
4282
  }
4283
+ if (error instanceof OauthCallbackError && options.recoveryMessage) {
4284
+ throw error.withMessage(
4285
+ withRecoveryMessage(error.message, options.recoveryMessage)
4286
+ );
4287
+ }
3971
4288
  if (error instanceof ZapierCliUserCancellationError && !options.silent) {
3972
4289
  log_default.info(`
3973
- \u274C ${flowName} cancelled by user`);
4290
+ \u274C ${options.copy.flowName} cancelled by user`);
3974
4291
  }
3975
4292
  throw error;
3976
4293
  }
@@ -3978,12 +4295,22 @@ async function runOauthFlowEntryPoint({
3978
4295
  function withRecoveryMessage(message, recoveryMessage) {
3979
4296
  return recoveryMessage ? `${message} ${recoveryMessage}` : message;
3980
4297
  }
4298
+ function createMissingHeadlessCallbackUrlError() {
4299
+ return new OauthCallbackError({
4300
+ kind: "missing_callback_url",
4301
+ message: "Paste the final OAuth callback URL from your browser."
4302
+ });
4303
+ }
4304
+ function writeHeadlessOauthStatus(message) {
4305
+ process.stderr.write(`${message}
4306
+ `);
4307
+ }
3981
4308
  async function runOauthFlow({
3982
4309
  timeoutMs = LOGIN_TIMEOUT_MS,
3983
4310
  pkceCredentials,
3984
4311
  baseUrl: baseUrl2,
3985
4312
  entryPoint,
3986
- authAction,
4313
+ copy,
3987
4314
  silent = false,
3988
4315
  onProgress
3989
4316
  }) {
@@ -3998,7 +4325,7 @@ async function runOauthFlow({
3998
4325
  const code = await collectLocalCallbackCode({
3999
4326
  transaction,
4000
4327
  timeoutMs,
4001
- authAction,
4328
+ action: copy.action,
4002
4329
  silent,
4003
4330
  onProgress
4004
4331
  });
@@ -4012,92 +4339,104 @@ async function runOauthFlow({
4012
4339
  }
4013
4340
  async function readHeadlessCallbackUrl({
4014
4341
  timeoutMs,
4015
- interactive,
4016
- recoveryMessage
4342
+ interactive
4017
4343
  }) {
4018
- const timeoutMessage = withRecoveryMessage(
4019
- `Signup timed out after ${Math.round(timeoutMs / 1e3)} seconds.`,
4020
- recoveryMessage
4021
- );
4022
- const missingCallbackUrlMessage = withRecoveryMessage(
4023
- "Paste the final OAuth callback URL from your browser.",
4024
- recoveryMessage
4025
- );
4026
4344
  const rl = createInterface({ input: process.stdin, output: process.stderr });
4027
4345
  const abortController = new AbortController();
4028
4346
  const timeoutTimer = setTimeout(() => abortController.abort(), timeoutMs);
4029
- const readUrl = interactive ? rl.question("Paste the final OAuth callback URL: ", {
4030
- signal: abortController.signal
4031
- }) : new Promise((resolve4, reject) => {
4347
+ const readUrl = new Promise((resolve4, reject) => {
4032
4348
  let settled = false;
4033
- const settleResolve = (value) => {
4349
+ const handleLine = (value) => settleResolve(value);
4350
+ const handleAbort = () => settleReject(new OauthFlowTimeoutError({ timeoutMs }));
4351
+ const handleClose = () => settleReject(createMissingHeadlessCallbackUrlError());
4352
+ const handleError = (error) => settleReject(error);
4353
+ const handleCancellation = () => settleReject(new ZapierCliUserCancellationError());
4354
+ const cleanupListeners = () => {
4355
+ abortController.signal.removeEventListener("abort", handleAbort);
4356
+ rl.off("line", handleLine);
4357
+ rl.off("close", handleClose);
4358
+ rl.off("error", handleError);
4359
+ rl.off("SIGINT", handleCancellation);
4360
+ process.off("SIGINT", handleCancellation);
4361
+ process.off("SIGTERM", handleCancellation);
4362
+ };
4363
+ function settleResolve(value) {
4364
+ if (settled) return;
4034
4365
  settled = true;
4366
+ cleanupListeners();
4035
4367
  resolve4(value);
4036
- };
4037
- const settleReject = (error) => {
4368
+ }
4369
+ function settleReject(error) {
4038
4370
  if (settled) return;
4039
4371
  settled = true;
4372
+ cleanupListeners();
4040
4373
  reject(error);
4041
- };
4042
- abortController.signal.addEventListener(
4043
- "abort",
4044
- () => settleReject(new Error(timeoutMessage)),
4045
- { once: true }
4046
- );
4047
- rl.once("line", settleResolve);
4048
- rl.once(
4049
- "close",
4050
- () => settleReject(new ZapierCliValidationError(missingCallbackUrlMessage))
4051
- );
4052
- rl.once("error", settleReject);
4374
+ }
4375
+ abortController.signal.addEventListener("abort", handleAbort, {
4376
+ once: true
4377
+ });
4378
+ rl.once("close", handleClose);
4379
+ rl.once("error", handleError);
4380
+ rl.once("SIGINT", handleCancellation);
4381
+ process.once("SIGINT", handleCancellation);
4382
+ process.once("SIGTERM", handleCancellation);
4383
+ if (interactive) {
4384
+ void rl.question("Paste the final OAuth callback URL: ", {
4385
+ signal: abortController.signal
4386
+ }).then(settleResolve).catch((error) => {
4387
+ if (error instanceof Error && error.name === "AbortError") {
4388
+ settleReject(new OauthFlowTimeoutError({ timeoutMs }));
4389
+ return;
4390
+ }
4391
+ settleReject(error);
4392
+ });
4393
+ return;
4394
+ }
4395
+ rl.once("line", handleLine);
4053
4396
  });
4054
4397
  try {
4055
- return await readUrl.catch((error) => {
4056
- if (error instanceof Error && error.name === "AbortError") {
4057
- throw new Error(timeoutMessage);
4058
- }
4059
- throw error;
4060
- });
4398
+ return await readUrl;
4061
4399
  } finally {
4062
4400
  clearTimeout(timeoutTimer);
4063
4401
  rl.close();
4064
4402
  }
4065
4403
  }
4066
- async function runHeadlessSignupOauthFlow({
4404
+ async function runHeadlessOauthFlow({
4067
4405
  timeoutMs = LOGIN_TIMEOUT_MS,
4068
4406
  pkceCredentials,
4069
4407
  baseUrl: baseUrl2,
4408
+ entryPoint,
4070
4409
  interactive = true,
4071
4410
  onProgress,
4072
- recoveryMessage
4411
+ copy
4073
4412
  }) {
4074
4413
  const port = LOGIN_PORTS[0];
4075
4414
  const transaction = await prepareOauthTransaction({
4076
4415
  pkceCredentials,
4077
4416
  baseUrl: baseUrl2,
4078
4417
  redirectUri: `http://${OAUTH_LOOPBACK_HOST}:${port}/oauth`,
4079
- entryPoint: "signup"
4418
+ entryPoint
4080
4419
  });
4081
- console.log(
4082
- "Use this mode when signing up from a machine that has no browser."
4420
+ writeHeadlessOauthStatus(
4421
+ `Use this mode to ${copy.action} from a machine that has no browser.`
4422
+ );
4423
+ writeHeadlessOauthStatus(
4424
+ `Open this ${copy.urlLabel} in a browser on another machine:`
4083
4425
  );
4084
- console.log("Open this signup URL in a browser on another machine:");
4085
4426
  console.log(transaction.browserAuthUrl);
4086
- console.log(
4427
+ writeHeadlessOauthStatus(
4087
4428
  `When the browser lands on ${transaction.redirectUri} and cannot connect, paste the full final URL back here.`
4088
4429
  );
4089
4430
  const callbackUrl = await readHeadlessCallbackUrl({
4090
4431
  timeoutMs,
4091
- interactive,
4092
- recoveryMessage
4432
+ interactive
4093
4433
  });
4094
4434
  const code = getCallbackCode({
4095
4435
  callbackUrl,
4096
- transaction,
4097
- recoveryMessage
4436
+ transaction
4098
4437
  });
4099
4438
  onProgress?.({ type: "callback_accepted" });
4100
- console.log("Exchanging authorization code for tokens...");
4439
+ writeHeadlessOauthStatus("Exchanging authorization code for tokens...");
4101
4440
  onProgress?.({ type: "token_exchange_started" });
4102
4441
  const tokens = await exchangeOauthCode({ ...transaction, code });
4103
4442
  onProgress?.({ type: "token_exchange_completed" });
@@ -4106,7 +4445,7 @@ async function runHeadlessSignupOauthFlow({
4106
4445
  async function collectLocalCallbackCode({
4107
4446
  transaction,
4108
4447
  timeoutMs,
4109
- authAction,
4448
+ action,
4110
4449
  silent,
4111
4450
  onProgress
4112
4451
  }) {
@@ -4114,21 +4453,26 @@ async function collectLocalCallbackCode({
4114
4453
  const app = express();
4115
4454
  app.get("/oauth", (req, res) => {
4116
4455
  res.setHeader("Connection", "close");
4117
- if (req.query.state !== transaction.state) {
4118
- res.status(400).end("Invalid state. You can close this tab.");
4119
- } else if (req.query.error) {
4120
- reject(
4121
- new OauthAuthorizationDeniedError(
4122
- String(req.query.error_description ?? req.query.error)
4123
- )
4124
- );
4125
- res.end("Authorization was denied. You can close this tab.");
4126
- } else if (!req.query.code) {
4127
- reject(new Error("No authorization code received"));
4128
- res.end("No authorization code received. You can close this tab.");
4129
- } else {
4130
- resolve4(String(req.query.code));
4456
+ try {
4457
+ const code = getCallbackCode({
4458
+ callbackUrl: new URL(
4459
+ req.originalUrl,
4460
+ transaction.redirectUri
4461
+ ).toString(),
4462
+ transaction
4463
+ });
4464
+ resolve4(code);
4131
4465
  res.end("You can now close this tab and return to the CLI.");
4466
+ } catch (error) {
4467
+ if (error instanceof OauthCallbackError && error.kind === "state_mismatch") {
4468
+ res.status(400).end("Invalid state. You can close this tab.");
4469
+ } else if (error instanceof OauthAuthorizationDeniedError) {
4470
+ reject(error);
4471
+ res.end("Authorization was denied. You can close this tab.");
4472
+ } else {
4473
+ reject(error);
4474
+ res.end("No authorization code received. You can close this tab.");
4475
+ }
4132
4476
  }
4133
4477
  });
4134
4478
  const server = app.listen(
@@ -4149,19 +4493,19 @@ async function collectLocalCallbackCode({
4149
4493
  let timeoutTimer;
4150
4494
  try {
4151
4495
  await waitForServerListening(server);
4152
- await openBrowser({ transaction, authAction, silent, onProgress });
4496
+ await openBrowser({ transaction, action, silent, onProgress });
4153
4497
  const waitForCode = Promise.race([
4154
4498
  promise,
4155
4499
  new Promise((_resolve, rejectTimeout) => {
4156
4500
  timeoutTimer = setTimeout(() => {
4157
- rejectTimeout(new OauthFlowTimeoutError(timeoutMs));
4501
+ rejectTimeout(new OauthFlowTimeoutError({ timeoutMs }));
4158
4502
  }, timeoutMs);
4159
4503
  })
4160
4504
  ]);
4161
4505
  onProgress?.({ type: "callback_waiting" });
4162
4506
  return silent ? await waitForCode : await spinPromise(
4163
4507
  waitForCode,
4164
- `Waiting for you to ${authAction} and authorize`
4508
+ `Waiting for you to ${action} and authorize`
4165
4509
  );
4166
4510
  } finally {
4167
4511
  if (timeoutTimer) clearTimeout(timeoutTimer);
@@ -4191,12 +4535,12 @@ async function waitForServerListening(server) {
4191
4535
  }
4192
4536
  async function openBrowser({
4193
4537
  transaction,
4194
- authAction,
4538
+ action,
4195
4539
  silent,
4196
4540
  onProgress
4197
4541
  }) {
4198
4542
  if (!silent) {
4199
- log_default.info(`Opening your browser to ${authAction}.`);
4543
+ log_default.info(`Opening your browser to ${action}.`);
4200
4544
  log_default.info("If it doesn't open, visit:", transaction.browserAuthUrl);
4201
4545
  }
4202
4546
  onProgress?.({ type: "browser_opening", url: transaction.browserAuthUrl });
@@ -4206,9 +4550,7 @@ async function openBrowser({
4206
4550
  } catch (err) {
4207
4551
  const reason = err instanceof Error ? err.message : String(err);
4208
4552
  if (!silent) {
4209
- log_default.info(
4210
- `Browser did not open automatically to ${authAction}: ${reason}`
4211
- );
4553
+ log_default.info(`Browser did not open automatically to ${action}: ${reason}`);
4212
4554
  log_default.info("Visit this URL manually:", transaction.browserAuthUrl);
4213
4555
  }
4214
4556
  onProgress?.({
@@ -4237,61 +4579,10 @@ async function closeServer({
4237
4579
  });
4238
4580
  }
4239
4581
 
4240
- // src/utils/auth/oauth-errors.ts
4241
- var SENSITIVE_OAUTH_FIELDS = [
4242
- "access_token",
4243
- "refresh_token",
4244
- "id_token",
4245
- "client_secret",
4246
- "code_verifier",
4247
- "code_challenge"
4248
- ];
4249
- function getErrorMessage(error) {
4250
- return error instanceof Error ? error.message : String(error);
4251
- }
4252
- function toCamelCase(field) {
4253
- return field.replace(
4254
- /_([a-z])/g,
4255
- (_match, letter) => letter.toUpperCase()
4256
- );
4257
- }
4258
- function escapeRegExp(value) {
4259
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4260
- }
4261
- var sensitiveOauthFieldPattern = Array.from(
4262
- new Set(
4263
- SENSITIVE_OAUTH_FIELDS.flatMap((field) => [field, toCamelCase(field)])
4264
- )
4265
- ).map(escapeRegExp).join("|");
4266
- var sensitiveQueryParamPattern = new RegExp(
4267
- `([?&])(${sensitiveOauthFieldPattern})(=)[^&#\\s"'<>]*`,
4268
- "gi"
4269
- );
4270
- function redactSensitiveOauthErrorMessage(message) {
4271
- return message.replace(
4272
- sensitiveQueryParamPattern,
4273
- (_match, prefix, key, separator) => `${prefix}${key}${separator}[REDACTED]`
4274
- ).replace(
4275
- new RegExp(`"(${sensitiveOauthFieldPattern})"(\\s*:\\s*)"[^"]*"`, "g"),
4276
- (_match, key, separator) => `"${key}"${separator}"[REDACTED]"`
4277
- );
4278
- }
4279
- function toRedactedOauthError(error) {
4280
- const message = redactSensitiveOauthErrorMessage(getErrorMessage(error));
4281
- if (error instanceof ZapierCliValidationError) {
4282
- return new ZapierCliValidationError(message);
4283
- }
4284
- if (error instanceof Error) {
4285
- const redactedError = new Error(message);
4286
- redactedError.name = error.name;
4287
- return redactedError;
4288
- }
4289
- return new ZapierCliValidationError(message);
4290
- }
4291
-
4292
4582
  // src/utils/auth/account-auth.ts
4293
4583
  var LEGACY_JWT_UPGRADE_PROMPT = "We're upgrading your login to client credentials for a simpler, more reliable experience and to support future security controls. Older Zapier SDK/CLI versions on this machine may stop working after the upgrade. Continue?";
4294
4584
  var SIGNUP_RECOVERY_MESSAGE = "Restart `zapier-sdk signup` to generate a fresh signup URL and try again.";
4585
+ var HEADLESS_LOGIN_RECOVERY_MESSAGE = "Restart `zapier-sdk login --headless` to generate a fresh login URL and try again.";
4295
4586
  var HEADLESS_SIGNUP_RECOVERY_MESSAGE = "Restart `zapier-sdk signup --headless` to generate a fresh signup URL and try again.";
4296
4587
  function getEntryPointLabel(entryPoint) {
4297
4588
  return entryPoint === "signup" ? "Signup" : "Login";
@@ -4411,7 +4702,8 @@ Logging out will delete these credentials and may interrupt other Zapier SDK or
4411
4702
  Log out and ${getActiveCredentialsAction(entryPoint)}?`
4412
4703
  }) : promptlessCredentialResetError(activeCredentials);
4413
4704
  if (!confirmed) {
4414
- console.log(`${flowLabel} cancelled.`);
4705
+ process.stderr.write(`${flowLabel} cancelled.
4706
+ `);
4415
4707
  return false;
4416
4708
  }
4417
4709
  try {
@@ -4430,7 +4722,8 @@ Log out and ${getActiveCredentialsAction(entryPoint)}?`
4430
4722
  message: `${flowLabel} cleanup failed. Reset local session state and continue?`
4431
4723
  });
4432
4724
  if (!reset) {
4433
- console.log(`${flowLabel} cancelled.`);
4725
+ process.stderr.write(`${flowLabel} cancelled.
4726
+ `);
4434
4727
  return false;
4435
4728
  }
4436
4729
  await deleteStoredClientCredentials({
@@ -4444,7 +4737,8 @@ Log out and ${getActiveCredentialsAction(entryPoint)}?`
4444
4737
  message: LEGACY_JWT_UPGRADE_PROMPT
4445
4738
  }) : promptlessLegacyJwtUpgradeError();
4446
4739
  if (!confirmed) {
4447
- console.log(`${flowLabel} cancelled.`);
4740
+ process.stderr.write(`${flowLabel} cancelled.
4741
+ `);
4448
4742
  return false;
4449
4743
  }
4450
4744
  }
@@ -4539,7 +4833,14 @@ async function runOauthForEntryPoint({
4539
4833
  );
4540
4834
  }
4541
4835
  return runOauthWithRedaction(
4542
- () => runLoginOauthFlow({ timeoutMs, pkceCredentials, baseUrl: baseUrl2 })
4836
+ () => runLoginOauthFlow({
4837
+ timeoutMs,
4838
+ pkceCredentials,
4839
+ baseUrl: baseUrl2,
4840
+ headless,
4841
+ interactive,
4842
+ recoveryMessage: headless ? HEADLESS_LOGIN_RECOVERY_MESSAGE : void 0
4843
+ })
4543
4844
  );
4544
4845
  }
4545
4846
  async function runAccountAuth({
@@ -4551,6 +4852,7 @@ async function runAccountAuth({
4551
4852
  const interactive = !resolveNonInteractive(options);
4552
4853
  const resolvedCredentials = await sdk.context.resolveCredentials();
4553
4854
  const pkceCredentials = toPkceCredentials(resolvedCredentials);
4855
+ const headless = options.headless === true;
4554
4856
  const credentialsBaseUrl2 = await resolveCredentialsBaseUrl({
4555
4857
  ...sdk.context,
4556
4858
  resolvedCredentials
@@ -4580,7 +4882,7 @@ async function runAccountAuth({
4580
4882
  timeoutMs: timeoutSeconds * 1e3,
4581
4883
  pkceCredentials,
4582
4884
  baseUrl: credentialsBaseUrl2,
4583
- headless: options.headless === true,
4885
+ headless,
4584
4886
  interactive
4585
4887
  });
4586
4888
  const scopedApi = getOrCreateApiClient({
@@ -4588,9 +4890,10 @@ async function runAccountAuth({
4588
4890
  baseUrl: credentialsBaseUrl2
4589
4891
  });
4590
4892
  const profile = await getProfile(scopedApi);
4591
- console.log(getProfileMessage(entryPoint, profile.email));
4592
- console.log(
4593
- "\nGenerating credentials so this machine can make authenticated requests on your behalf."
4893
+ process.stderr.write(`${getProfileMessage(entryPoint, profile.email)}
4894
+ `);
4895
+ process.stderr.write(
4896
+ "\nGenerating credentials so this machine can make authenticated requests on your behalf.\n"
4594
4897
  );
4595
4898
  const credentialName = providedName ?? await resolveCredentialName({
4596
4899
  email: profile.email,
@@ -4606,11 +4909,12 @@ async function runAccountAuth({
4606
4909
  useApprovals,
4607
4910
  cleanupLogPrefix: entryPoint
4608
4911
  });
4609
- console.log(
4610
- `\u2705 Credentials "${credentialName}" created and set as default. You are ready to use the Zapier SDK.`
4912
+ process.stderr.write(
4913
+ `\u2705 Credentials "${credentialName}" created and set as default. You are ready to use the Zapier SDK.
4914
+ `
4611
4915
  );
4612
4916
  if (useApprovals) {
4613
- console.log("\u{1F510} Approvals are enabled for these credentials.");
4917
+ process.stderr.write("\u{1F510} Approvals are enabled for these credentials.\n");
4614
4918
  }
4615
4919
  emitAccountAuthSuccess({ sdk, profile, clientId });
4616
4920
  }
@@ -4629,7 +4933,10 @@ var LoginSchema = z.object({
4629
4933
  skipPrompts: z.boolean().optional().meta({
4630
4934
  deprecated: true,
4631
4935
  deprecationMessage: "Use --non-interactive instead."
4632
- })
4936
+ }),
4937
+ headless: z.boolean().optional().describe(
4938
+ "Use when logging in from a machine that has no browser. Prints a login link to open elsewhere, then accepts the pasted loopback callback URL."
4939
+ )
4633
4940
  }).describe("Log in to Zapier to access your account");
4634
4941
 
4635
4942
  // src/plugins/login/index.ts
@@ -7261,7 +7568,7 @@ function buildBoxLines(message) {
7261
7568
  // package.json with { type: 'json' }
7262
7569
  var package_default2 = {
7263
7570
  name: "@zapier/zapier-sdk-cli",
7264
- version: "0.60.0"};
7571
+ version: "0.61.1"};
7265
7572
 
7266
7573
  // src/sdk.ts
7267
7574
  injectCliLogin(login_exports);
@@ -7338,6 +7645,9 @@ function createZapierCliSdk2(options = {}) {
7338
7645
 
7339
7646
  // src/utils/extensions.ts
7340
7647
  var ENV_VAR = "ZAPIER_SDK_EXTENSIONS";
7648
+ function isModelPlugin(value) {
7649
+ return typeof value === "object" && value !== null && "pluginType" in value && typeof value.pluginType === "string";
7650
+ }
7341
7651
  async function resolveExtensions() {
7342
7652
  const seen = /* @__PURE__ */ new Set();
7343
7653
  const specs = readEnvSpecs().filter((spec) => {
@@ -7370,8 +7680,11 @@ function normalizeExtension(exported) {
7370
7680
  if (typeof exported === "function") {
7371
7681
  return [exported];
7372
7682
  }
7683
+ if (isModelPlugin(exported)) {
7684
+ return [exported];
7685
+ }
7373
7686
  if (Array.isArray(exported)) {
7374
- if (exported.every((e) => typeof e === "function")) {
7687
+ if (exported.every((e) => typeof e === "function" || isModelPlugin(e))) {
7375
7688
  return exported;
7376
7689
  }
7377
7690
  return null;