@wildo-ai/external-connectors-models 1.1.11 → 1.1.13

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 (109) hide show
  1. package/dist/esm/companion-exports.d.ts.map +1 -1
  2. package/dist/esm/companion-exports.js.map +1 -1
  3. package/dist/esm/core/commons.external-connectors-models.schemas.d.ts +10 -0
  4. package/dist/esm/core/commons.external-connectors-models.schemas.d.ts.map +1 -1
  5. package/dist/esm/core/commons.external-connectors-models.schemas.js +10 -0
  6. package/dist/esm/core/commons.external-connectors-models.schemas.js.map +1 -1
  7. package/dist/esm/core/oauth2.enums.d.ts.map +1 -1
  8. package/dist/esm/core/oauth2.enums.js.map +1 -1
  9. package/dist/esm/core/operation-classification.external-connectors-models.schemas.d.ts.map +1 -1
  10. package/dist/esm/core/operation-classification.external-connectors-models.schemas.js.map +1 -1
  11. package/dist/esm/core/operations.external-connectors-models.schemas.d.ts.map +1 -1
  12. package/dist/esm/core/operations.external-connectors-models.schemas.js.map +1 -1
  13. package/dist/esm/core/security.external-connectors-models.schemas.d.ts.map +1 -1
  14. package/dist/esm/core/security.external-connectors-models.schemas.js.map +1 -1
  15. package/dist/esm/index.d.ts +6 -0
  16. package/dist/esm/index.d.ts.map +1 -1
  17. package/dist/esm/index.js +6 -0
  18. package/dist/esm/index.js.map +1 -1
  19. package/dist/esm/protocols/agent-client-protocol.external-connectors-models.schemas.d.ts +20 -46
  20. package/dist/esm/protocols/agent-client-protocol.external-connectors-models.schemas.d.ts.map +1 -1
  21. package/dist/esm/protocols/agent-client-protocol.external-connectors-models.schemas.js +34 -5
  22. package/dist/esm/protocols/agent-client-protocol.external-connectors-models.schemas.js.map +1 -1
  23. package/dist/esm/protocols/billing.standard-interface.schemas.d.ts +56 -3
  24. package/dist/esm/protocols/billing.standard-interface.schemas.d.ts.map +1 -1
  25. package/dist/esm/protocols/billing.standard-interface.schemas.js +15 -2
  26. package/dist/esm/protocols/billing.standard-interface.schemas.js.map +1 -1
  27. package/dist/esm/protocols/content-safety.external-connectors-models.schemas.d.ts.map +1 -1
  28. package/dist/esm/protocols/content-safety.external-connectors-models.schemas.js.map +1 -1
  29. package/dist/esm/protocols/document-extraction.external-connectors-models.schemas.d.ts.map +1 -1
  30. package/dist/esm/protocols/document-extraction.external-connectors-models.schemas.js.map +1 -1
  31. package/dist/esm/protocols/electronic-signature.external-connectors-models.schemas.d.ts +157 -0
  32. package/dist/esm/protocols/electronic-signature.external-connectors-models.schemas.d.ts.map +1 -0
  33. package/dist/esm/protocols/electronic-signature.external-connectors-models.schemas.js +134 -0
  34. package/dist/esm/protocols/electronic-signature.external-connectors-models.schemas.js.map +1 -0
  35. package/dist/esm/protocols/email-inbound.standard-interface.schemas.d.ts +85 -0
  36. package/dist/esm/protocols/email-inbound.standard-interface.schemas.d.ts.map +1 -1
  37. package/dist/esm/protocols/email-inbound.standard-interface.schemas.js +46 -2
  38. package/dist/esm/protocols/email-inbound.standard-interface.schemas.js.map +1 -1
  39. package/dist/esm/protocols/email.external-connectors-models.schemas.d.ts.map +1 -1
  40. package/dist/esm/protocols/email.external-connectors-models.schemas.js.map +1 -1
  41. package/dist/esm/protocols/email.standard-interface.schemas.d.ts.map +1 -1
  42. package/dist/esm/protocols/email.standard-interface.schemas.js.map +1 -1
  43. package/dist/esm/protocols/embeddings.external-connectors-models.schemas.d.ts +49 -6
  44. package/dist/esm/protocols/embeddings.external-connectors-models.schemas.d.ts.map +1 -1
  45. package/dist/esm/protocols/embeddings.external-connectors-models.schemas.js +89 -6
  46. package/dist/esm/protocols/embeddings.external-connectors-models.schemas.js.map +1 -1
  47. package/dist/esm/protocols/file-storage.standard-interface.d.ts +9 -2
  48. package/dist/esm/protocols/file-storage.standard-interface.d.ts.map +1 -1
  49. package/dist/esm/protocols/file-storage.standard-interface.js +6 -0
  50. package/dist/esm/protocols/file-storage.standard-interface.js.map +1 -1
  51. package/dist/esm/protocols/frontend.external-connectors-models.schemas.d.ts.map +1 -1
  52. package/dist/esm/protocols/frontend.external-connectors-models.schemas.js.map +1 -1
  53. package/dist/esm/protocols/graphql-operations.external-connectors-models.schemas.d.ts +254 -0
  54. package/dist/esm/protocols/graphql-operations.external-connectors-models.schemas.d.ts.map +1 -0
  55. package/dist/esm/protocols/graphql-operations.external-connectors-models.schemas.js +480 -0
  56. package/dist/esm/protocols/graphql-operations.external-connectors-models.schemas.js.map +1 -0
  57. package/dist/esm/protocols/hooks-capability.external-connectors-models.schemas.d.ts.map +1 -1
  58. package/dist/esm/protocols/hooks-capability.external-connectors-models.schemas.js.map +1 -1
  59. package/dist/esm/protocols/hooks.external-connectors-models.schemas.d.ts +95 -1
  60. package/dist/esm/protocols/hooks.external-connectors-models.schemas.d.ts.map +1 -1
  61. package/dist/esm/protocols/hooks.external-connectors-models.schemas.js +110 -0
  62. package/dist/esm/protocols/hooks.external-connectors-models.schemas.js.map +1 -1
  63. package/dist/esm/protocols/image.external-connectors-models.schemas.d.ts.map +1 -1
  64. package/dist/esm/protocols/image.external-connectors-models.schemas.js.map +1 -1
  65. package/dist/esm/protocols/llm.external-connectors-models.schemas.d.ts +95 -3
  66. package/dist/esm/protocols/llm.external-connectors-models.schemas.d.ts.map +1 -1
  67. package/dist/esm/protocols/llm.external-connectors-models.schemas.js +147 -15
  68. package/dist/esm/protocols/llm.external-connectors-models.schemas.js.map +1 -1
  69. package/dist/esm/protocols/oauth2-provider-assurance.definitions.d.ts.map +1 -1
  70. package/dist/esm/protocols/oauth2-provider-assurance.definitions.js.map +1 -1
  71. package/dist/esm/protocols/oauth2-provider-assurance.schemas.d.ts.map +1 -1
  72. package/dist/esm/protocols/oauth2-provider-assurance.schemas.js.map +1 -1
  73. package/dist/esm/protocols/oauth2.external-connectors-models.schemas.d.ts.map +1 -1
  74. package/dist/esm/protocols/oauth2.external-connectors-models.schemas.js.map +1 -1
  75. package/dist/esm/protocols/provider-configuration-authoring.external-connectors-models.schemas.d.ts +52 -0
  76. package/dist/esm/protocols/provider-configuration-authoring.external-connectors-models.schemas.d.ts.map +1 -0
  77. package/dist/esm/protocols/provider-configuration-authoring.external-connectors-models.schemas.js +132 -0
  78. package/dist/esm/protocols/provider-configuration-authoring.external-connectors-models.schemas.js.map +1 -0
  79. package/dist/esm/protocols/provider-connection-parameters.external-connectors-models.d.ts +111 -0
  80. package/dist/esm/protocols/provider-connection-parameters.external-connectors-models.d.ts.map +1 -0
  81. package/dist/esm/protocols/provider-connection-parameters.external-connectors-models.js +203 -0
  82. package/dist/esm/protocols/provider-connection-parameters.external-connectors-models.js.map +1 -0
  83. package/dist/esm/protocols/provider-oauth2-token-endpoint-hosts.external-connectors-models.d.ts +6 -0
  84. package/dist/esm/protocols/provider-oauth2-token-endpoint-hosts.external-connectors-models.d.ts.map +1 -1
  85. package/dist/esm/protocols/provider-oauth2-token-endpoint-hosts.external-connectors-models.js +13 -7
  86. package/dist/esm/protocols/provider-oauth2-token-endpoint-hosts.external-connectors-models.js.map +1 -1
  87. package/dist/esm/protocols/provider-vendor-registration.external-connectors-models.d.ts +50 -0
  88. package/dist/esm/protocols/provider-vendor-registration.external-connectors-models.d.ts.map +1 -0
  89. package/dist/esm/protocols/provider-vendor-registration.external-connectors-models.js +30 -0
  90. package/dist/esm/protocols/provider-vendor-registration.external-connectors-models.js.map +1 -0
  91. package/dist/esm/protocols/rest-operations.external-connectors-models.schemas.d.ts +730 -17
  92. package/dist/esm/protocols/rest-operations.external-connectors-models.schemas.d.ts.map +1 -1
  93. package/dist/esm/protocols/rest-operations.external-connectors-models.schemas.js +822 -20
  94. package/dist/esm/protocols/rest-operations.external-connectors-models.schemas.js.map +1 -1
  95. package/dist/esm/protocols/service-account-token.external-connectors-models.schemas.d.ts +73 -0
  96. package/dist/esm/protocols/service-account-token.external-connectors-models.schemas.d.ts.map +1 -0
  97. package/dist/esm/protocols/service-account-token.external-connectors-models.schemas.js +82 -0
  98. package/dist/esm/protocols/service-account-token.external-connectors-models.schemas.js.map +1 -0
  99. package/dist/esm/protocols/sms.external-connectors-models.schemas.d.ts.map +1 -1
  100. package/dist/esm/protocols/sms.external-connectors-models.schemas.js.map +1 -1
  101. package/dist/esm/protocols/sms.standard-interface.schemas.d.ts +79 -1
  102. package/dist/esm/protocols/sms.standard-interface.schemas.d.ts.map +1 -1
  103. package/dist/esm/protocols/sms.standard-interface.schemas.js +93 -0
  104. package/dist/esm/protocols/sms.standard-interface.schemas.js.map +1 -1
  105. package/dist/esm/protocols/subject-erasure-effect.standard-interface.d.ts.map +1 -1
  106. package/dist/esm/protocols/subject-erasure-effect.standard-interface.js.map +1 -1
  107. package/dist/tsconfig.build.tsbuildinfo +1 -1
  108. package/package.json +5 -5
  109. package/dist/esm/.native.tsbuildinfo +0 -1
@@ -43,6 +43,7 @@
43
43
  * OpenAPI document is ever read (a second mapper input), it feeds `ProviderRestOperation` like
44
44
  * every other source.
45
45
  */
46
+ import { ExternalProvider_Connection_OwnerScope } from '../core/security.external-connectors-models.schemas.js';
46
47
  /** @wildo_source:part:start external-connectors.models.rest-operation-contract facet:layer:shared facet:family:provider facet:audience:app-developer */
47
48
  /** Where a declared parameter rides on the wire. */
48
49
  export var ProviderRestParameterLocation;
@@ -109,6 +110,129 @@ export var ProviderRestPaginationStyle;
109
110
  /** The vendor returns its own continuation URL (`@odata.nextLink`, `links.next`). */
110
111
  ProviderRestPaginationStyle["LINK"] = "link";
111
112
  })(ProviderRestPaginationStyle || (ProviderRestPaginationStyle = {}));
113
+ /**
114
+ * The schema a dot path into an operation's input names, or `undefined` when a segment names no declared field (#3399).
115
+ *
116
+ * Walked through Zod's own definition (`_zod.def`), not `instanceof`: a provider module may be built against another
117
+ * copy of Zod than the host's. Wrappers that leave the value's shape alone (optional, nullable, default, prefault,
118
+ * readonly, catch, nonoptional, a pipe's input, a lazy getter) are seen through. A union names the field when any of its
119
+ * options declares it, as a discriminated body does. Only an object's DECLARED fields count: a loose object admits other
120
+ * keys at runtime, and a picker for a key nobody declared is the typo this check exists to refuse.
121
+ */
122
+ function providerRestInputSchemaAtPath(schema, segments) {
123
+ let current = schema;
124
+ for (let depth = 0; depth < 64; depth += 1) {
125
+ const def = current?._zod?.def;
126
+ if (def === undefined)
127
+ return undefined;
128
+ switch (def.type) {
129
+ case 'optional':
130
+ case 'nullable':
131
+ case 'default':
132
+ case 'prefault':
133
+ case 'readonly':
134
+ case 'catch':
135
+ case 'nonoptional':
136
+ current = def.innerType;
137
+ continue;
138
+ case 'pipe':
139
+ current = def.in;
140
+ continue;
141
+ case 'lazy':
142
+ current = typeof def.getter === 'function' ? def.getter() : undefined;
143
+ continue;
144
+ case 'union': {
145
+ if (segments.length === 0)
146
+ return current;
147
+ const options = Array.isArray(def.options) ? def.options : [];
148
+ return options.map((option) => providerRestInputSchemaAtPath(option, segments)).find((found) => found !== undefined);
149
+ }
150
+ case 'object': {
151
+ if (segments.length === 0)
152
+ return current;
153
+ const shape = def.shape;
154
+ const [head, ...rest] = segments;
155
+ if (shape === undefined || head === undefined || !Object.hasOwn(shape, head))
156
+ return undefined;
157
+ return providerRestInputSchemaAtPath(shape[head], rest);
158
+ }
159
+ default:
160
+ return segments.length === 0 ? current : undefined;
161
+ }
162
+ }
163
+ return undefined;
164
+ }
165
+ /**
166
+ * Why an operation's `inputOptions` cannot be honoured, or an empty list (#3399). Each key, and each `dependsOn` value,
167
+ * is a dot path into the operation's input (`project.key`, `CustomerRef.value`; a top-level field is a one-segment
168
+ * path), and must name a field the operation's `inputSchema` declares. Checked where a catalogue loads (the operation
169
+ * executor, an augmentation or supplement merge) and at publish, so a mistyped path is refused once, by name, instead of
170
+ * offering a picker that fills nothing. An operation without an `inputSchema` passes its input through unvalidated and
171
+ * declares no fields, so there is nothing to check its paths against; only their shape is.
172
+ */
173
+ export function describeProviderRestOperationInputOptionsDefects(operation) {
174
+ const defects = [];
175
+ const check = (path, role) => {
176
+ const segments = path.split('.');
177
+ if (segments.some((segment) => segment.length === 0)) {
178
+ defects.push(`operation '${operation.operationId}': ${role} '${path}' is not a dot path of field names`);
179
+ return;
180
+ }
181
+ if (operation.inputSchema !== undefined && providerRestInputSchemaAtPath(operation.inputSchema, segments) === undefined) {
182
+ defects.push(`operation '${operation.operationId}': ${role} '${path}' names no field of its inputSchema`);
183
+ }
184
+ };
185
+ for (const [inputPath, declaration] of Object.entries(operation.inputOptions ?? {})) {
186
+ check(inputPath, 'the inputOptions key');
187
+ for (const sourcePath of Object.values(declaration.dependsOn ?? {}))
188
+ check(sourcePath, `the dependsOn of '${inputPath}'`);
189
+ for (const fixed of Object.keys(declaration.input ?? {})) {
190
+ if (Object.hasOwn(declaration.dependsOn ?? {}, fixed)) {
191
+ defects.push(`operation '${operation.operationId}': the options of '${inputPath}' both fix and forward '${fixed}'`);
192
+ }
193
+ }
194
+ for (const listPath of [declaration.itemsPath, ...Object.keys(declaration.itemWhere ?? {})]) {
195
+ if (listPath !== undefined && listPath.split('.').some((segment) => segment.length === 0)) {
196
+ defects.push(`operation '${operation.operationId}': the options of '${inputPath}' name '${listPath}', which is not a dot path`);
197
+ }
198
+ }
199
+ }
200
+ return defects;
201
+ }
202
+ /**
203
+ * Why an operation's {@link ProviderRestOperationParameter.connectionFact} parameters cannot be honoured, or an empty
204
+ * list (#3397). Each must be a PATH parameter the path template uses, fed by nothing else (no `from`, `constantValue` or
205
+ * `multiSegment`), and the operation's `inputSchema` must not declare a field of its name: a caller is never asked for a
206
+ * value the connection supplies, and one it sends anyway is refused at the call. With `reportedAccountFacts` (the
207
+ * connection's OAuth2 declaration), each named fact must also be declared there with a `pathSegmentPattern`; without
208
+ * it the request compiler refuses the call, so a module walk passes them to catch it before any call.
209
+ */
210
+ export function describeProviderRestOperationConnectionFactDefects(operation, reportedAccountFacts) {
211
+ const defects = [];
212
+ for (const parameter of operation.parameters ?? []) {
213
+ const factName = parameter.connectionFact;
214
+ if (factName === undefined)
215
+ continue;
216
+ const at = `operation '${operation.operationId}': the parameter '${parameter.name}' filled from the connection fact '${factName}'`;
217
+ if (parameter.in !== ProviderRestParameterLocation.PATH)
218
+ defects.push(`${at} is a ${parameter.in} parameter; only a PATH value may come from a connection fact`);
219
+ if (parameter.from !== undefined || parameter.constantValue !== undefined || parameter.multiSegment !== undefined) {
220
+ defects.push(`${at} also declares from, constantValue or multiSegment; its value comes from the connection alone`);
221
+ }
222
+ if (!operation.pathTemplate.includes(`{${parameter.name}}`))
223
+ defects.push(`${at} fills no '{${parameter.name}}' token of the path template`);
224
+ if (operation.inputSchema !== undefined && providerRestInputSchemaAtPath(operation.inputSchema, [parameter.name]) !== undefined) {
225
+ defects.push(`${at} is also declared by the inputSchema, which would ask callers for a value the connection supplies`);
226
+ }
227
+ if (reportedAccountFacts !== undefined) {
228
+ const fact = Object.hasOwn(reportedAccountFacts, factName) ? reportedAccountFacts[factName] : undefined;
229
+ if (fact?.pathSegmentPattern === undefined) {
230
+ defects.push(`${at}: the connection's OAuth2 contract declares no reported fact '${factName}' with a pathSegmentPattern`);
231
+ }
232
+ }
233
+ }
234
+ return defects;
235
+ }
112
236
  /** What a successful HTTP answer said about the call, read through a {@link ProviderRestApplicationOutcome}. */
113
237
  export var ProviderRestApplicationOutcomeKind;
114
238
  (function (ProviderRestApplicationOutcomeKind) {
@@ -151,6 +275,22 @@ export function describeProviderRestApplicationOutcomeDefects(outcome) {
151
275
  export const PROVIDER_CLIENT_CREDENTIALS_TENANT_PLACEHOLDER = '{tenant}';
152
276
  /** A tenant value the token URL admits: a GUID or a DNS name. Nothing that can leave the URL's path segment. */
153
277
  export const PROVIDER_CLIENT_CREDENTIALS_TENANT_SHAPE = /^[A-Za-z0-9](?:[A-Za-z0-9.-]{0,251}[A-Za-z0-9])?$/;
278
+ /**
279
+ * The placeholder that BEGINS a client-credentials token URL whose token endpoint is on the deployment's own vendor
280
+ * instance (#3402): `{deploymentOrigin}/oauth2/v1/token`. Filled with the ORIGIN of the base URL the engine resolved for
281
+ * the REST contract (from `baseUrlHostEnvVarName` or `baseUrlEnvVarName`), never from a connection or a request.
282
+ *
283
+ * Okta is the case: every org has its own host (`<org>.okta.com`, or a custom domain), and its org authorization server's
284
+ * token endpoint is on that host. A fixed {@link ProviderRestClientCredentials.tokenHosts} list cannot admit a host only
285
+ * the operator knows, and a `{tenant}` value would be a second, separately typed copy of the host the operations already
286
+ * go to, which could disagree with it. Filling the token endpoint from the SAME resolved origin keeps them one value: the
287
+ * client's assertion is sent to the host the operator named, and to nothing a person connecting could choose.
288
+ *
289
+ * spec-cite: Okta Developer, "Implement OAuth for Okta with a service app"
290
+ * (https://developer.okta.com/docs/guides/implement-oauth-for-okta-serviceapp/main/, fetched 2026-11-10) — the token
291
+ * request is `POST https://{yourOktaDomain}/oauth2/v1/token`, and the assertion's `aud` is that URL.
292
+ */
293
+ export const PROVIDER_CLIENT_CREDENTIALS_DEPLOYMENT_ORIGIN_PLACEHOLDER = '{deploymentOrigin}';
154
294
  /** What a {@link ProviderClientCredentialsVerifiedFact} records: a value the person typed and the vendor accepted. */
155
295
  export var ProviderClientCredentialsFactSource;
156
296
  (function (ProviderClientCredentialsFactSource) {
@@ -159,6 +299,90 @@ export var ProviderClientCredentialsFactSource;
159
299
  /** The tenant whose token endpoint issued it. */
160
300
  ProviderClientCredentialsFactSource["TENANT"] = "tenant";
161
301
  })(ProviderClientCredentialsFactSource || (ProviderClientCredentialsFactSource = {}));
302
+ /**
303
+ * The `grant_type` a client-credentials token request sends. Narrower than `OAuth2_GrantType` on purpose: these are the
304
+ * grants the client-credentials minter performs, each with nothing but the client's own credential and the declared
305
+ * parameters, and a member joins in the change that implements it.
306
+ */
307
+ export var ProviderClientCredentialsGrantType;
308
+ (function (ProviderClientCredentialsGrantType) {
309
+ /**
310
+ * The OAuth 2.0 client-credentials grant: the default.
311
+ * spec-cite: RFC 6749 §4.4.2 — `grant_type`: "REQUIRED. Value MUST be set to "client_credentials"." Fetched 2026-10-06.
312
+ */
313
+ ProviderClientCredentialsGrantType["CLIENT_CREDENTIALS"] = "client_credentials";
314
+ /**
315
+ * Zoom's server-to-server OAuth: the same exchange (a client authenticated with Basic, one token back, no refresh
316
+ * token) under Zoom's own grant name, with the account named in `account_id`.
317
+ * spec-cite: https://developers.zoom.us/docs/internal-apps/s2s-oauth/ — "Use the `account_credentials` grant type to
318
+ * generate an access token."; "Set up the body of the access token request using the `account_credentials`
319
+ * `grant_type` and your `account_id`." Fetched 2026-11-09.
320
+ */
321
+ ProviderClientCredentialsGrantType["ACCOUNT_CREDENTIALS"] = "account_credentials";
322
+ })(ProviderClientCredentialsGrantType || (ProviderClientCredentialsGrantType = {}));
323
+ /**
324
+ * The algorithms a client assertion is signed with (#3402). The JWS names (RFC 7518 §3.1) of the two asymmetric
325
+ * families every vendor measured accepts: Okta lists "RS256, RS384, RS512, ES256, ES384, and ES512" for a service app's
326
+ * key. A member joins when a vendor needs it.
327
+ * spec-cite: RFC 7518 §3.1 — "RS256 | RSASSA-PKCS1-v1_5 using SHA-256" and "ES256 | ECDSA using P-256 and SHA-256";
328
+ * https://developer.okta.com/docs/guides/implement-oauth-for-okta-serviceapp/main/ — the supported algorithms. Both
329
+ * fetched 2026-11-09.
330
+ */
331
+ export var ProviderClientAssertionSigningAlgorithm;
332
+ (function (ProviderClientAssertionSigningAlgorithm) {
333
+ /** ECDSA on P-256 with SHA-256: a short key and signature, and the default to choose for a new key. */
334
+ ProviderClientAssertionSigningAlgorithm["ES256"] = "ES256";
335
+ /** RSASSA-PKCS1-v1_5 with SHA-256, for a vendor or key store that issues RSA keys. */
336
+ ProviderClientAssertionSigningAlgorithm["RS256"] = "RS256";
337
+ })(ProviderClientAssertionSigningAlgorithm || (ProviderClientAssertionSigningAlgorithm = {}));
338
+ /** The longest a client assertion may live: Okta refuses one whose `exp` is more than an hour away. */
339
+ export const PROVIDER_CLIENT_ASSERTION_MAXIMUM_LIFETIME_SECONDS = 3600;
340
+ /** How long a client assertion lives when the declaration says nothing: long enough for one request, and no more. */
341
+ export const PROVIDER_CLIENT_ASSERTION_DEFAULT_LIFETIME_SECONDS = 300;
342
+ /**
343
+ * The slot of a client-credentials declaration's own CREDENTIAL: the client secret, or the private key (#3402).
344
+ * `undefined` only for a declaration {@link describeProviderRestClientCredentialsDefects} refuses.
345
+ */
346
+ export function providerClientCredentialsCredentialSlot(credentials) {
347
+ return credentials.clientSecretSecretRef ?? credentials.privateKeyJwt?.privateKeySecretRef;
348
+ }
349
+ /**
350
+ * The parameter names {@link ProviderRestClientCredentials.tokenRequestParameters} may not declare: each is written by
351
+ * the grant itself (the grant type, the client's credential, the scope, a client assertion), so a second value would
352
+ * either be ignored or override the declared one.
353
+ */
354
+ export const PROVIDER_CLIENT_CREDENTIALS_RESERVED_PARAMETER_NAMES = [
355
+ 'grant_type', 'client_id', 'client_secret', 'scope', 'client_assertion', 'client_assertion_type',
356
+ ];
357
+ /**
358
+ * Whether the client-credentials door of a REST contract serves a connection of this owner scope (#3398).
359
+ *
360
+ * Alone, it serves every admitted connection: the application's own (from its environment) and an organization's (from
361
+ * the credential the organization typed, as a Teams bot). Beside `oauth2`, a person or an organization connects through
362
+ * the authorization-code exchange, and the client-credentials grant serves the APPLICATION's own account only, so that
363
+ * account renews itself instead of resting on a pasted token that expires within the hour.
364
+ */
365
+ export function providerClientCredentialsServesOwnerScope(contract, ownerScope) {
366
+ if (contract.clientCredentials === undefined)
367
+ return false;
368
+ return contract.oauth2 === undefined || ownerScope === ExternalProvider_Connection_OwnerScope.APPLICATION;
369
+ }
370
+ /**
371
+ * The slot a connection of this owner scope AUTHENTICATES with, as the engine's REST access resolver reads it: the
372
+ * client-credentials grant's own credential (its client secret or private key) when that door serves the owner scope
373
+ * ({@link providerClientCredentialsServesOwnerScope}), else `apiKey.secretRef`, the stored or pasted token itself.
374
+ *
375
+ * For a contract declaring `clientCredentials` beside `oauth2` (Box, Zoom, #3398) the two differ by owner: the
376
+ * application's own account mints from the server app's secret, while a person's connection reads its exchanged token
377
+ * from `apiKey.secretRef`. A tool standing in for one connection (`wildo providers probe`, `connect`, the doctor's
378
+ * configuration checks, all APPLICATION-owned) asks this instead of reading `apiKey.secretRef`, so it reads the slot the
379
+ * engine would. `undefined` when the contract names neither.
380
+ */
381
+ export function providerRestConnectionCredentialSlot(contract, ownerScope) {
382
+ return providerClientCredentialsServesOwnerScope(contract, ownerScope)
383
+ ? providerClientCredentialsCredentialSlot(contract.clientCredentials)
384
+ : contract.apiKey?.secretRef;
385
+ }
162
386
  /**
163
387
  * Why a client-credentials declaration cannot be honoured, or an empty list. Checked where a provider is composed:
164
388
  * a token URL that is not https, a token host that is not a DNS name or that the URL is not on, a tenant placeholder
@@ -167,9 +391,19 @@ export var ProviderClientCredentialsFactSource;
167
391
  export function describeProviderRestClientCredentialsDefects(credentials) {
168
392
  const defects = [];
169
393
  const hasTenant = credentials.tokenUrl.includes(PROVIDER_CLIENT_CREDENTIALS_TENANT_PLACEHOLDER);
170
- const filled = credentials.tokenUrl.split(PROVIDER_CLIENT_CREDENTIALS_TENANT_PLACEHOLDER).join('tenant.example');
394
+ // The deployment's origin forms the WHOLE origin or nothing (#3402): text before it, or anything but a path after it,
395
+ // would compose a host the operator never named.
396
+ const onDeploymentOrigin = credentials.tokenUrl.includes(PROVIDER_CLIENT_CREDENTIALS_DEPLOYMENT_ORIGIN_PLACEHOLDER);
397
+ if (onDeploymentOrigin && !credentials.tokenUrl.startsWith(`${PROVIDER_CLIENT_CREDENTIALS_DEPLOYMENT_ORIGIN_PLACEHOLDER}/`)) {
398
+ defects.push(`clientCredentials.tokenUrl carries ${PROVIDER_CLIENT_CREDENTIALS_DEPLOYMENT_ORIGIN_PLACEHOLDER} other than as its whole origin, followed by a path`);
399
+ }
400
+ if (onDeploymentOrigin && hasTenant)
401
+ defects.push(`clientCredentials.tokenUrl carries both ${PROVIDER_CLIENT_CREDENTIALS_DEPLOYMENT_ORIGIN_PLACEHOLDER} and ${PROVIDER_CLIENT_CREDENTIALS_TENANT_PLACEHOLDER}; the token endpoint has one source`);
402
+ const filled = credentials.tokenUrl
403
+ .split(PROVIDER_CLIENT_CREDENTIALS_TENANT_PLACEHOLDER).join('tenant.example')
404
+ .split(PROVIDER_CLIENT_CREDENTIALS_DEPLOYMENT_ORIGIN_PLACEHOLDER).join('https://deployment.example');
171
405
  if (/[{}]/.test(filled))
172
- defects.push(`clientCredentials.tokenUrl '${credentials.tokenUrl}' carries a placeholder other than ${PROVIDER_CLIENT_CREDENTIALS_TENANT_PLACEHOLDER}`);
406
+ defects.push(`clientCredentials.tokenUrl '${credentials.tokenUrl}' carries a placeholder other than ${PROVIDER_CLIENT_CREDENTIALS_TENANT_PLACEHOLDER} and ${PROVIDER_CLIENT_CREDENTIALS_DEPLOYMENT_ORIGIN_PLACEHOLDER}`);
173
407
  if (hasTenant && credentials.tenantSecretRef === undefined)
174
408
  defects.push(`clientCredentials.tokenUrl carries ${PROVIDER_CLIENT_CREDENTIALS_TENANT_PLACEHOLDER} and no tenantSecretRef fills it`);
175
409
  if (!hasTenant && credentials.tenantSecretRef !== undefined)
@@ -177,17 +411,66 @@ export function describeProviderRestClientCredentialsDefects(credentials) {
177
411
  const host = /^https:\/\/([^/:?#]+)(?::\d+)?\//.exec(filled)?.[1]?.toLowerCase();
178
412
  if (host === undefined)
179
413
  defects.push(`clientCredentials.tokenUrl '${credentials.tokenUrl}' is not an https URL with a path`);
180
- if (credentials.tokenHosts.length === 0)
181
- defects.push('clientCredentials.tokenHosts admits no host');
182
- for (const tokenHost of credentials.tokenHosts) {
183
- if (!PROVIDER_OAUTH2_DNS_NAME_PATTERN.test(tokenHost.toLowerCase()))
184
- defects.push(`clientCredentials.tokenHosts names '${tokenHost}', which is not a DNS name`);
414
+ if (onDeploymentOrigin) {
415
+ // The host is the deployment's, admitted where the engine resolves the operations' base URL; a fixed list would be
416
+ // a second, unchecked statement of it.
417
+ if (credentials.tokenHosts.length > 0)
418
+ defects.push(`clientCredentials.tokenHosts names hosts beside ${PROVIDER_CLIENT_CREDENTIALS_DEPLOYMENT_ORIGIN_PLACEHOLDER}; the token host is the deployment's, so declare none`);
419
+ }
420
+ else {
421
+ if (credentials.tokenHosts.length === 0)
422
+ defects.push('clientCredentials.tokenHosts admits no host');
423
+ for (const tokenHost of credentials.tokenHosts) {
424
+ if (!PROVIDER_OAUTH2_DNS_NAME_PATTERN.test(tokenHost.toLowerCase()))
425
+ defects.push(`clientCredentials.tokenHosts names '${tokenHost}', which is not a DNS name`);
426
+ }
427
+ if (host !== undefined && !credentials.tokenHosts.some((tokenHost) => tokenHost.toLowerCase() === host)) {
428
+ defects.push(`clientCredentials.tokenUrl is on '${host}', which tokenHosts does not admit`);
429
+ }
430
+ }
431
+ if (credentials.scope !== undefined && credentials.scope.trim() === '')
432
+ defects.push('clientCredentials.scope is empty; omit it to send none');
433
+ const grantTypes = Object.values(ProviderClientCredentialsGrantType);
434
+ if (credentials.grantType !== undefined && !grantTypes.includes(credentials.grantType)) {
435
+ defects.push(`clientCredentials.grantType ${JSON.stringify(credentials.grantType)} is none of ${grantTypes.join(', ')}`);
185
436
  }
186
- if (host !== undefined && !credentials.tokenHosts.some((tokenHost) => tokenHost.toLowerCase() === host)) {
187
- defects.push(`clientCredentials.tokenUrl is on '${host}', which tokenHosts does not admit`);
437
+ // One credential: a secret or a private key (#3402), and the method agrees with which.
438
+ if ((credentials.clientSecretSecretRef === undefined) === (credentials.privateKeyJwt === undefined)) {
439
+ defects.push('clientCredentials declares exactly one of clientSecretSecretRef and privateKeyJwt');
440
+ }
441
+ const methods = credentials.privateKeyJwt !== undefined
442
+ ? [ProviderOAuth2ClientAuthenticationMethod.PRIVATE_KEY_JWT]
443
+ : [ProviderOAuth2ClientAuthenticationMethod.CLIENT_SECRET_POST, ProviderOAuth2ClientAuthenticationMethod.CLIENT_SECRET_BASIC];
444
+ if (credentials.tokenEndpointAuthMethod !== undefined && !methods.includes(credentials.tokenEndpointAuthMethod)) {
445
+ defects.push(`clientCredentials.tokenEndpointAuthMethod ${JSON.stringify(credentials.tokenEndpointAuthMethod)} is none of ${methods.join(', ')} for ${credentials.privateKeyJwt !== undefined ? 'a private key' : 'a client secret'}`);
446
+ }
447
+ if (credentials.privateKeyJwt !== undefined) {
448
+ const { privateKeySecretRef, algorithm, keyIdSecretRef, assertionLifetimeSeconds } = credentials.privateKeyJwt;
449
+ const algorithms = Object.values(ProviderClientAssertionSigningAlgorithm);
450
+ if (privateKeySecretRef.trim() === '')
451
+ defects.push('clientCredentials.privateKeyJwt.privateKeySecretRef names no slot');
452
+ if (!algorithms.includes(algorithm))
453
+ defects.push(`clientCredentials.privateKeyJwt.algorithm ${JSON.stringify(algorithm)} is none of ${algorithms.join(', ')}`);
454
+ if (keyIdSecretRef !== undefined && keyIdSecretRef.trim() === '')
455
+ defects.push('clientCredentials.privateKeyJwt.keyIdSecretRef names no slot');
456
+ if (assertionLifetimeSeconds !== undefined
457
+ && !(Number.isInteger(assertionLifetimeSeconds) && assertionLifetimeSeconds > 0 && assertionLifetimeSeconds <= PROVIDER_CLIENT_ASSERTION_MAXIMUM_LIFETIME_SECONDS)) {
458
+ defects.push(`clientCredentials.privateKeyJwt.assertionLifetimeSeconds is not a whole number of seconds from 1 to ${PROVIDER_CLIENT_ASSERTION_MAXIMUM_LIFETIME_SECONDS}`);
459
+ }
460
+ }
461
+ for (const [name, parameter] of Object.entries(credentials.tokenRequestParameters ?? {})) {
462
+ if (!/^[A-Za-z][A-Za-z0-9_.-]{0,63}$/.test(name))
463
+ defects.push(`clientCredentials.tokenRequestParameters names '${name}', which is not a parameter name`);
464
+ if (PROVIDER_CLIENT_CREDENTIALS_RESERVED_PARAMETER_NAMES.includes(name)) {
465
+ defects.push(`clientCredentials.tokenRequestParameters names '${name}', which the grant itself sends; declare it in its own field`);
466
+ }
467
+ const hasValue = typeof parameter.value === 'string';
468
+ const hasSlot = typeof parameter.secretRef === 'string';
469
+ if (hasValue === hasSlot)
470
+ defects.push(`clientCredentials.tokenRequestParameters '${name}' must declare exactly one of value and secretRef`);
471
+ else if ((parameter.value ?? parameter.secretRef ?? '').trim() === '')
472
+ defects.push(`clientCredentials.tokenRequestParameters '${name}' is empty`);
188
473
  }
189
- if (credentials.scope.trim() === '')
190
- defects.push('clientCredentials.scope is empty');
191
474
  for (const [name, fact] of Object.entries(credentials.verifiedAccountFacts ?? {})) {
192
475
  if (!/^[A-Za-z][A-Za-z0-9_]{0,63}$/.test(name))
193
476
  defects.push(`clientCredentials.verifiedAccountFacts names '${name}', which is not a fact name`);
@@ -200,6 +483,59 @@ export function describeProviderRestClientCredentialsDefects(credentials) {
200
483
  }
201
484
  return defects;
202
485
  }
486
+ /**
487
+ * Why a REST contract's `clientCredentials` sits wrongly beside the contract's other authentication fields, or an empty
488
+ * list (#2663, #3398). Checked where a provider is composed, beside {@link describeProviderRestClientCredentialsDefects}.
489
+ *
490
+ * - beside `requestSigning`: two ways to authenticate one call;
491
+ * - alone, `apiKey.secretRef` must be the client secret's slot, the one whose absence means "not configured";
492
+ * - beside `oauth2` the grant serves the APPLICATION's own account only ({@link providerClientCredentialsServesOwnerScope}),
493
+ * so the module must admit that connection; `apiKey.secretRef` names the exchanged tokens' slot and may not be the
494
+ * client secret's (a person's token would be read from it); and `verifiedAccountFacts`, which only an organization's
495
+ * connection records, would record nothing.
496
+ * - a token URL on {@link PROVIDER_CLIENT_CREDENTIALS_DEPLOYMENT_ORIGIN_PLACEHOLDER} (#3402) needs a base URL the
497
+ * DEPLOYMENT names (`baseUrlHostEnvVarName` or `baseUrlEnvVarName`), and may serve the APPLICATION's connection only:
498
+ * the deployment's instance is the application's own (its Okta org), so an organization minting there would
499
+ * authenticate against the application's vendor tenant, never its own.
500
+ */
501
+ export function describeProviderRestClientCredentialsPlacementDefects(contract, admitsApplicationConnection,
502
+ /** Whether the module admits an ORGANIZATION or USER connection. Omitted, `false`. */
503
+ admitsNonApplicationConnection = false) {
504
+ const credentials = contract.clientCredentials;
505
+ if (credentials === undefined)
506
+ return [];
507
+ const defects = [];
508
+ if (contract.requestSigning !== undefined) {
509
+ defects.push('declares clientCredentials beside requestSigning; a call authenticates one way');
510
+ }
511
+ if (credentials.tokenUrl.includes(PROVIDER_CLIENT_CREDENTIALS_DEPLOYMENT_ORIGIN_PLACEHOLDER)) {
512
+ if (contract.baseUrlHostEnvVarName === undefined && contract.baseUrlEnvVarName === undefined) {
513
+ defects.push(`clientCredentials.tokenUrl is on ${PROVIDER_CLIENT_CREDENTIALS_DEPLOYMENT_ORIGIN_PLACEHOLDER}, and the contract's base URL is not one the deployment names (baseUrlHostEnvVarName or baseUrlEnvVarName)`);
514
+ }
515
+ if (!admitsApplicationConnection) {
516
+ defects.push(`clientCredentials.tokenUrl is on ${PROVIDER_CLIENT_CREDENTIALS_DEPLOYMENT_ORIGIN_PLACEHOLDER}, which serves the application's own connection only, and the module admits none`);
517
+ }
518
+ if (contract.oauth2 === undefined && admitsNonApplicationConnection) {
519
+ defects.push(`clientCredentials.tokenUrl is on ${PROVIDER_CLIENT_CREDENTIALS_DEPLOYMENT_ORIGIN_PLACEHOLDER}, the application's own vendor instance, and the module admits an organization or user connection that would mint there`);
520
+ }
521
+ }
522
+ const credentialSlot = providerClientCredentialsCredentialSlot(credentials);
523
+ if (contract.oauth2 === undefined) {
524
+ if (contract.apiKey.secretRef !== credentialSlot) {
525
+ defects.push(`apiKey.secretRef '${contract.apiKey.secretRef}' is not the client's credential slot '${credentialSlot}' (its secret or private key); that is the slot whose absence means not configured`);
526
+ }
527
+ return defects;
528
+ }
529
+ if (!admitsApplicationConnection)
530
+ defects.push('declares clientCredentials beside oauth2 and admits no APPLICATION connection, the only one that grant serves');
531
+ if (contract.apiKey.secretRef === credentialSlot) {
532
+ defects.push(`apiKey.secretRef '${contract.apiKey.secretRef}' is the client-credentials secret or key; beside oauth2 it names the slot of the exchanged access tokens`);
533
+ }
534
+ if (credentials.verifiedAccountFacts !== undefined) {
535
+ defects.push('declares clientCredentials.verifiedAccountFacts beside oauth2; an organization connects through oauth2, so no client-credentials fact is ever recorded');
536
+ }
537
+ return defects;
538
+ }
203
539
  function readDotPath(body, path) {
204
540
  let current = body;
205
541
  for (const segment of path.split('.')) {
@@ -350,17 +686,15 @@ export var ProviderOAuth2ScopeMeaning;
350
686
  */
351
687
  ProviderOAuth2ScopeMeaning["PERMISSION"] = "PERMISSION";
352
688
  })(ProviderOAuth2ScopeMeaning || (ProviderOAuth2ScopeMeaning = {}));
353
- /** Whether `host` is admitted by one audience's hosts: an exact value, or `<labels><suffix>` for a declared suffix. */
689
+ /**
690
+ * Whether `host` is admitted by one audience's hosts: an exact value, or `<labels><suffix>` for a declared suffix, with
691
+ * no more labels than `hostSuffixMaxLabels` allows (#3640).
692
+ */
354
693
  export function providerOAuth2AudienceAdmitsHost(audience, host) {
355
694
  const candidate = host.toLowerCase();
356
695
  if (audience.hosts.values.some((value) => value.toLowerCase() === candidate))
357
696
  return true;
358
- return (audience.hosts.hostSuffixes ?? []).some((suffix) => {
359
- const lowered = suffix.toLowerCase();
360
- if (!candidate.endsWith(lowered) || candidate.length === lowered.length)
361
- return false;
362
- return PROVIDER_OAUTH2_DNS_NAME_PATTERN.test(candidate.slice(0, candidate.length - lowered.length));
363
- });
697
+ return (audience.hosts.hostSuffixes ?? []).some((suffix) => providerHostIsUnderSuffix(candidate, suffix, audience.hosts.hostSuffixMaxLabels));
364
698
  }
365
699
  /**
366
700
  * The ONE audience whose hosts admit `host`, or `undefined` when none does. The declaration guarantees at most one
@@ -527,6 +861,76 @@ export var ProviderOAuth2PkceSupport;
527
861
  /** The vendor refuses PKCE from a confidential client (Slack): neither is sent. */
528
862
  ProviderOAuth2PkceSupport["NOT_ACCEPTED"] = "NOT_ACCEPTED";
529
863
  })(ProviderOAuth2PkceSupport || (ProviderOAuth2PkceSupport = {}));
864
+ /**
865
+ * How this application's client authenticates to a vendor's token and revocation endpoints. See
866
+ * {@link ProviderRestOAuth2.tokenEndpointAuthMethod}.
867
+ *
868
+ * The values are the names the "OAuth Token Endpoint Authentication Methods" registry gives these two methods, which
869
+ * is what OpenID Connect and dynamic client registration call `token_endpoint_auth_method`, so a vendor's discovery
870
+ * document (`token_endpoint_auth_methods_supported`) can be compared with a declaration by eye.
871
+ * spec-cite: RFC 7591 §2 — `token_endpoint_auth_method`: "client_secret_post": "The client uses the HTTP POST parameters
872
+ * as defined in OAuth 2.0, Section 2.3.1."; "client_secret_basic": "The client uses HTTP Basic as defined in OAuth 2.0,
873
+ * Section 2.3.1." Fetched 2026-11-06.
874
+ *
875
+ * Narrower than `OAuth2_TokenEndpoint_AuthMethod` on purpose: that vocabulary also names `client_secret_jwt` and
876
+ * `none`, which nothing here performs. `private_key_jwt` joined with #3402, performed by the client-credentials grant
877
+ * only; the connected-account exchange refuses it at startup. A member nothing honours would
878
+ * be a declaration that looks like a client-authentication choice and changes nothing, so each method joins this
879
+ * vocabulary in the change that implements it.
880
+ */
881
+ export var ProviderOAuth2ClientAuthenticationMethod;
882
+ (function (ProviderOAuth2ClientAuthenticationMethod) {
883
+ /**
884
+ * `client_id` and `client_secret` as parameters of the request body: the engine's behaviour when nothing is declared,
885
+ * and what most vendors' own documentation shows. RFC 6749 calls it "NOT RECOMMENDED", which is why a vendor that
886
+ * documents Basic declares {@link CLIENT_SECRET_BASIC} rather than being met with this.
887
+ * spec-cite: RFC 6749 §2.3.1 — sending the two parameters in the body "is NOT RECOMMENDED and SHOULD be limited to
888
+ * clients unable to directly utilize the HTTP Basic authentication scheme". Fetched 2026-11-06.
889
+ */
890
+ ProviderOAuth2ClientAuthenticationMethod["CLIENT_SECRET_POST"] = "client_secret_post";
891
+ /**
892
+ * `Authorization: Basic base64(client_id ":" client_secret)`, each half form-urlencoded BEFORE base64, and neither in
893
+ * the body: one credential on one channel. Every authorization server must accept it (Zoom, Notion and Intuit document
894
+ * it as their client authentication).
895
+ * spec-cite: RFC 6749 §2.3.1 — "The client identifier is encoded using the "application/x-www-form-urlencoded"
896
+ * encoding algorithm per Appendix B", and so is the password, before both become the Basic user-id and password; "The
897
+ * authorization server MUST support the HTTP Basic authentication scheme". Appendix B encodes UTF-8 first, so `£` is
898
+ * `%C2%A3` and a space is `+`. Fetched 2026-11-06.
899
+ */
900
+ ProviderOAuth2ClientAuthenticationMethod["CLIENT_SECRET_BASIC"] = "client_secret_basic";
901
+ /**
902
+ * A JWT the client signs with its own private key, sent as `client_assertion` (#3402). Performed by the
903
+ * client-credentials grant only ({@link ProviderRestClientCredentials.privateKeyJwt}); the connected-account exchange
904
+ * does not sign assertions, so `oauth2.tokenEndpointAuthMethod` refuses it at startup.
905
+ * spec-cite: OpenID Connect Core 1.0 §9 — `private_key_jwt`: "Clients that have registered a public key sign a JWT
906
+ * using that key." Fetched 2026-11-09 from https://openid.net/specs/openid-connect-core-1_0.html.
907
+ */
908
+ ProviderOAuth2ClientAuthenticationMethod["PRIVATE_KEY_JWT"] = "private_key_jwt";
909
+ })(ProviderOAuth2ClientAuthenticationMethod || (ProviderOAuth2ClientAuthenticationMethod = {}));
910
+ /**
911
+ * How the parameters of a request to a vendor's token or revocation endpoint are written in its body. See
912
+ * {@link ProviderRestOAuth2.tokenRequestBodyEncoding} and {@link ProviderRestOAuth2.revocationRequestBodyEncoding}.
913
+ *
914
+ * The parameters are the same in both encodings (`grant_type`, `code`, `refresh_token`, `token`, …); only their
915
+ * serialization and the `Content-Type` change.
916
+ */
917
+ export var ProviderOAuth2RequestBodyEncoding;
918
+ (function (ProviderOAuth2RequestBodyEncoding) {
919
+ /**
920
+ * `application/x-www-form-urlencoded`, which RFC 6749 and RFC 7009 specify: the default for both requests.
921
+ * spec-cite: RFC 6749 §4.1.3 (the client makes the token request "by sending the following parameters using the
922
+ * "application/x-www-form-urlencoded" format"; §6 does the same for a refresh), and RFC 7009 §2.1 (the revocation
923
+ * request uses "the "application/x-www-form-urlencoded" format in the HTTP request entity-body"). Fetched 2026-11-06.
924
+ */
925
+ ProviderOAuth2RequestBodyEncoding["FORM"] = "FORM";
926
+ /**
927
+ * `application/json`, an object whose members are the same parameters as string values. A deviation some vendors
928
+ * document: Notion's token endpoint takes only JSON, and Intuit's own client library revokes with a JSON body.
929
+ * spec-cite: https://developers.notion.com/docs/authorization — the token request's `Content-Type: application/json`,
930
+ * fetched 2026-11-06.
931
+ */
932
+ ProviderOAuth2RequestBodyEncoding["JSON"] = "JSON";
933
+ })(ProviderOAuth2RequestBodyEncoding || (ProviderOAuth2RequestBodyEncoding = {}));
530
934
  /**
531
935
  * A `scope` list encoding that deviates from RFC 6749's space-delimited list, in either direction: the token answer
532
936
  * ({@link ProviderRestOAuth2.grantedScopesDelimiter}) or the authorize request ({@link ProviderRestOAuth2.authorizeScopesDelimiter}).
@@ -544,6 +948,87 @@ export var ProviderOAuth2ScopeListDelimiter;
544
948
  */
545
949
  ProviderOAuth2ScopeListDelimiter["COMMA"] = "COMMA";
546
950
  })(ProviderOAuth2ScopeListDelimiter || (ProviderOAuth2ScopeListDelimiter = {}));
951
+ /**
952
+ * What makes a client-authentication declaration unusable (see {@link ProviderRestOAuth2.tokenEndpointAuthMethod}):
953
+ *
954
+ * - a method or an encoding outside its vocabulary. The request builder tests for the declared member, so an unknown
955
+ * value (a misspelling in a module written without types) would silently fall back to the default and send the
956
+ * secret the way the vendor said not to;
957
+ * - a revocation encoding with no revocation URL, a setting that changes nothing.
958
+ */
959
+ export function describeProviderOAuth2ClientAuthenticationDefects(oauth2) {
960
+ const defects = [];
961
+ // The exchange signs no assertion: a key-based client (#3402) authenticates only on the client-credentials grant.
962
+ const methods = [ProviderOAuth2ClientAuthenticationMethod.CLIENT_SECRET_POST, ProviderOAuth2ClientAuthenticationMethod.CLIENT_SECRET_BASIC];
963
+ const encodings = Object.values(ProviderOAuth2RequestBodyEncoding);
964
+ if (oauth2.tokenEndpointAuthMethod !== undefined && !methods.includes(oauth2.tokenEndpointAuthMethod)) {
965
+ defects.push(`declares tokenEndpointAuthMethod ${JSON.stringify(oauth2.tokenEndpointAuthMethod)}, which is none of ${methods.join(', ')}`);
966
+ }
967
+ for (const [field, value] of [['tokenRequestBodyEncoding', oauth2.tokenRequestBodyEncoding], ['revocationRequestBodyEncoding', oauth2.revocationRequestBodyEncoding]]) {
968
+ if (value !== undefined && !encodings.includes(value))
969
+ defects.push(`declares ${field} ${JSON.stringify(value)}, which is none of ${encodings.join(', ')}`);
970
+ }
971
+ if (oauth2.revocationRequestBodyEncoding !== undefined && oauth2.rfc7009RevocationUrl === undefined) {
972
+ defects.push('declares revocationRequestBodyEncoding without rfc7009RevocationUrl, so no revocation request uses it');
973
+ }
974
+ return defects;
975
+ }
976
+ /**
977
+ * The parameter names the token request writes itself, which {@link ProviderRestOAuth2.codeExchangeParameters} may not
978
+ * name: the exchange's own parameters (RFC 6749 §4.1.3), the client's credentials (§2.3.1) and assertion (RFC 7523 §2.2),
979
+ * and the refresh's (§6). A declared value for one of them would change what the request IS.
980
+ * semantic-literal-boundary: OAuth2 wire parameter names, as RFC 6749 and RFC 7523 spell them.
981
+ */
982
+ const PROVIDER_OAUTH2_ENGINE_WRITTEN_TOKEN_PARAMETERS = new Set([
983
+ 'grant_type', 'code', 'redirect_uri', 'code_verifier', 'client_id', 'client_secret', 'client_assertion', 'client_assertion_type', 'refresh_token', 'scope',
984
+ ]);
985
+ /** An OAuth2 error code's shape (RFC 6749 §5.2: `error` values are `%x20-21 / %x23-5B / %x5D-7E`, no space in practice). */
986
+ const PROVIDER_OAUTH2_ERROR_CODE_SHAPE = /^[A-Za-z0-9_.:-]{1,128}$/;
987
+ /** A token-request parameter name a declaration may add: a plain identifier, so it cannot smuggle structure into a body. */
988
+ const PROVIDER_OAUTH2_EXTRA_PARAMETER_NAME = /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/;
989
+ /**
990
+ * What makes a token-request declaration unusable (follow-up to #3392), checked where the provider registry composes:
991
+ *
992
+ * - a {@link ProviderRestOAuth2.codeExchangeParameters} name that is not a plain identifier, that names a parameter the
993
+ * engine writes itself, or whose value is empty;
994
+ * - a {@link ProviderRestOAuth2.refreshRefusals} entry whose code is not code-shaped (it would never match), is
995
+ * `invalid_grant` (already a refusal, so the entry says nothing) or `invalid_client` (the application's registration,
996
+ * never one person's grant), or whose description is empty.
997
+ */
998
+ export function describeProviderOAuth2TokenRequestDefects(oauth2) {
999
+ const defects = [];
1000
+ for (const [name, value] of Object.entries(oauth2.codeExchangeParameters ?? {})) {
1001
+ if (!PROVIDER_OAUTH2_EXTRA_PARAMETER_NAME.test(name))
1002
+ defects.push(`declares the code-exchange parameter ${JSON.stringify(name)}, which is not a plain parameter name`);
1003
+ else if (PROVIDER_OAUTH2_ENGINE_WRITTEN_TOKEN_PARAMETERS.has(name))
1004
+ defects.push(`declares the code-exchange parameter '${name}', which the engine writes itself`);
1005
+ if (typeof value !== 'string' || value.length === 0)
1006
+ defects.push(`declares the code-exchange parameter ${JSON.stringify(name)} with no value`);
1007
+ }
1008
+ for (const refusal of oauth2.refreshRefusals ?? []) {
1009
+ if (typeof refusal.error !== 'string' || !PROVIDER_OAUTH2_ERROR_CODE_SHAPE.test(refusal.error)) {
1010
+ defects.push(`declares the refresh refusal ${JSON.stringify(refusal.error)}, which is not an OAuth2 error code and would never match`);
1011
+ }
1012
+ else if (refusal.error === 'invalid_grant') {
1013
+ defects.push('declares the refresh refusal \'invalid_grant\', which already retires a connection');
1014
+ }
1015
+ else if (refusal.error === 'invalid_client') {
1016
+ defects.push('declares the refresh refusal \'invalid_client\', which names the application\'s registration, not a person\'s grant');
1017
+ }
1018
+ if (refusal.errorDescription !== undefined && refusal.errorDescription.trim().length === 0) {
1019
+ defects.push(`declares the refresh refusal '${refusal.error}' with an empty errorDescription`);
1020
+ }
1021
+ }
1022
+ return defects;
1023
+ }
1024
+ /**
1025
+ * Whether a refresh's refusal is one the provider declares as a dead refresh token ({@link ProviderRestOAuth2.refreshRefusals}).
1026
+ * The one reading, shared by the refresh and its tests.
1027
+ */
1028
+ export function providerOAuth2RefreshRefusalIsDeclared(oauth2, answer) {
1029
+ return (oauth2.refreshRefusals ?? []).some((refusal) => refusal.error === answer.error
1030
+ && (refusal.errorDescription === undefined || refusal.errorDescription === answer.errorDescription));
1031
+ }
547
1032
  /**
548
1033
  * The granted scopes a token answer's `scope` string names, read with the vendor's declared delimiter: whitespace (RFC
549
1034
  * 6749 §5.1) when none is declared, a comma for {@link ProviderOAuth2ScopeListDelimiter.COMMA}.
@@ -595,6 +1080,8 @@ export function describeProviderOAuth2AudienceDefects(oauth2) {
595
1080
  if (!PROVIDER_OAUTH2_DNS_NAME_PATTERN.test(value.toLowerCase()))
596
1081
  defects.push(`audience '${id}' declares the host '${value}', which is not a DNS name`);
597
1082
  }
1083
+ for (const defect of describeProviderHostSuffixMaxLabelsDefects(audience.hosts.hostSuffixMaxLabels, audience.hosts.hostSuffixes))
1084
+ defects.push(`audience '${id}': ${defect}`);
598
1085
  if (audience.mintScopes.length === 0)
599
1086
  defects.push(`audience '${id}' declares no mint scope`);
600
1087
  for (const scope of audience.mintScopes) {
@@ -618,7 +1105,7 @@ export function describeProviderOAuth2AudienceDefects(oauth2) {
618
1105
  const [rightId, rightAudience] = entries[right];
619
1106
  const overlap = leftAudience.hosts.values.some((host) => providerOAuth2AudienceAdmitsHost(rightAudience, host))
620
1107
  || rightAudience.hosts.values.some((host) => providerOAuth2AudienceAdmitsHost(leftAudience, host))
621
- || (leftAudience.hosts.hostSuffixes ?? []).some((suffix) => (rightAudience.hosts.hostSuffixes ?? []).some((other) => suffix.toLowerCase().endsWith(other.toLowerCase()) || other.toLowerCase().endsWith(suffix.toLowerCase())));
1108
+ || (leftAudience.hosts.hostSuffixes ?? []).some((suffix) => (rightAudience.hosts.hostSuffixes ?? []).some((other) => providerHostSuffixesOverlap({ suffix, ...(leftAudience.hosts.hostSuffixMaxLabels === undefined ? {} : { maxLabels: leftAudience.hosts.hostSuffixMaxLabels }) }, { suffix: other, ...(rightAudience.hosts.hostSuffixMaxLabels === undefined ? {} : { maxLabels: rightAudience.hosts.hostSuffixMaxLabels }) })));
622
1109
  if (overlap)
623
1110
  defects.push(`audiences '${leftId}' and '${rightId}' admit a common host; a request host must select one token`);
624
1111
  }
@@ -639,7 +1126,122 @@ export var ProviderOAuth2ReportedFactSource;
639
1126
  * spec-cite: OpenID Connect Core 1.0 §3.1.3.7, item 6.
640
1127
  */
641
1128
  ProviderOAuth2ReportedFactSource["ID_TOKEN_CLAIMS"] = "id_token_claims";
1129
+ /**
1130
+ * A query parameter of the AUTHORIZATION REDIRECT that brings the person back with the code (#3397): Intuit's
1131
+ * `realmId`, the QuickBooks company the person chose on the consent screen. `responsePath` is the parameter's name,
1132
+ * one segment. Read by the callback leg, kept with the attempt until the completion leg redeems the code, and recorded
1133
+ * only from that exchange: a refresh has no redirect, so it never restates such a fact and the stored value stands.
1134
+ *
1135
+ * The weakest of the three sources, and restricted for it. The redirect reaches this application through the
1136
+ * browser of the person who authorized, so that person could change the value before it arrives, exactly as they
1137
+ * could type an answer. Such a fact is therefore admitted only where a wrong value harms nobody but the person who
1138
+ * forged it: as a PATH value ({@link ProviderOAuth2ReportedAccountFact.pathSegmentPattern}) of a vendor that checks it
1139
+ * against the token on every call (Intuit answers a realm the token was not granted for with an authorization fault).
1140
+ * It may never be an `origin`, fill a base URL host, or route inbound deliveries, since each of those decides where a
1141
+ * credential goes or which organization receives a message; `describeProviderOAuth2ReportedFactUseDefects` refuses it.
1142
+ */
1143
+ ProviderOAuth2ReportedFactSource["AUTHORIZATION_REDIRECT_QUERY"] = "authorization_redirect_query";
1144
+ /**
1145
+ * The JSON answer of the vendor's USERINFO endpoint ({@link ProviderRestOAuth2.userInfoUrl}, #3397), fetched with the
1146
+ * access token the authorization just minted: DocuSign's `accounts[…].account_id` and `base_uri`. As trustworthy as
1147
+ * the token answer (the vendor answers over TLS, to a request the person's own token authorized), so it may be placed
1148
+ * wherever a token fact may. Read at the authorization-code exchange only: a refresh does not call the endpoint, so it
1149
+ * never restates such a fact and the stored value stands until the person connects again.
1150
+ */
1151
+ ProviderOAuth2ReportedFactSource["USERINFO_RESPONSE"] = "userinfo_response";
642
1152
  })(ProviderOAuth2ReportedFactSource || (ProviderOAuth2ReportedFactSource = {}));
1153
+ /** What reading a {@link ProviderOAuth2ReportedAccountFact.responsePath} out of a vendor's answer found (#3397). */
1154
+ export var ProviderReportedFactReadOutcome;
1155
+ (function (ProviderReportedFactReadOutcome) {
1156
+ /** A non-empty string or a finite number stood at the path; `value` is its text. */
1157
+ ProviderReportedFactReadOutcome["FOUND"] = "FOUND";
1158
+ /** Nothing usable stood at the path: a missing key, a `null`, an object, an empty string. The fact is not reported. */
1159
+ ProviderReportedFactReadOutcome["ABSENT"] = "ABSENT";
1160
+ /**
1161
+ * A selection segment (`accounts[is_default=true]`) met an array in which NOT EXACTLY ONE element matched. The vendor's
1162
+ * answer does not name one element, so no fact is read from it: guessing the first would address a request to an
1163
+ * account nobody chose. `matched` is how many did.
1164
+ */
1165
+ ProviderReportedFactReadOutcome["SELECTION_NOT_SINGLE"] = "SELECTION_NOT_SINGLE";
1166
+ })(ProviderReportedFactReadOutcome || (ProviderReportedFactReadOutcome = {}));
1167
+ /** `name` or `name[field=value]`; no `.`, `[`, `]` or `=` inside a name, a field or a value. */
1168
+ const PROVIDER_REPORTED_FACT_PATH_SEGMENT_PATTERN = /^([^.[\]=]+)(?:\[([^.[\]=]+)=([^.[\]=]*)\])?$/;
1169
+ /**
1170
+ * The steps of a reported fact's `responsePath`, or `undefined` when it is not one (#3397). A path is dot-separated;
1171
+ * each step is an own property name, optionally followed by ONE selection `[field=value]` that picks, out of the array
1172
+ * at that property, the single element whose `field` reads as `value`: `accounts[is_default=true].account_id`.
1173
+ *
1174
+ * Names, fields and values may not contain `.`, `[`, `]` or `=`; a value may be empty. Kept this small on purpose: it
1175
+ * addresses one element by one stated condition, which is what a vendor documenting "the default account" means, and
1176
+ * nothing that would need a query language.
1177
+ */
1178
+ export function parseProviderReportedFactPath(path) {
1179
+ if (typeof path !== 'string' || path.length === 0)
1180
+ return undefined;
1181
+ const segments = [];
1182
+ for (const text of path.split('.')) {
1183
+ const match = PROVIDER_REPORTED_FACT_PATH_SEGMENT_PATTERN.exec(text);
1184
+ if (match === null)
1185
+ return undefined;
1186
+ segments.push(match[2] === undefined ? { key: match[1] } : { key: match[1], select: { field: match[2], equals: match[3] } });
1187
+ }
1188
+ return segments;
1189
+ }
1190
+ /** The text a scalar reads as: a non-empty string, a finite number's decimal text, a boolean's `true`/`false`. */
1191
+ function providerReportedScalarText(value, admitBoolean) {
1192
+ if (typeof value === 'string')
1193
+ return value.length > 0 ? value : undefined;
1194
+ if (typeof value === 'number' && Number.isFinite(value))
1195
+ return String(value);
1196
+ if (admitBoolean && typeof value === 'boolean')
1197
+ return String(value);
1198
+ return undefined;
1199
+ }
1200
+ /**
1201
+ * The value at a reported fact's `responsePath` inside a vendor's JSON answer (#3397): a token answer, the claims of
1202
+ * its ID token, or a userinfo answer. Own properties only (the answer is a vendor's JSON, and `constructor` is not a
1203
+ * fact). A FOUND value is a non-empty string or a finite number's decimal text.
1204
+ *
1205
+ * A selection step keeps the ONE array element whose field reads as the stated value (a string as itself, a number as
1206
+ * its decimal text, a boolean as `true` / `false`, so DocuSign's `is_default: true` matches `[is_default=true]` whether
1207
+ * it is sent as a boolean or a string). Zero or several matches is {@link ProviderReportedFactReadOutcome.SELECTION_NOT_SINGLE}:
1208
+ * two facts selected with the same condition (`accounts[is_default=true].account_id` and `….base_uri`) are therefore
1209
+ * read from the same element or not at all. Anything else that does not lead to a scalar (a missing key, a property
1210
+ * that is not an array where a selection is written, an unparsable path) is ABSENT.
1211
+ */
1212
+ export function readProviderReportedFactValue(payload, path) {
1213
+ const segments = parseProviderReportedFactPath(path);
1214
+ if (segments === undefined)
1215
+ return { outcome: ProviderReportedFactReadOutcome.ABSENT };
1216
+ let current = payload;
1217
+ for (const segment of segments) {
1218
+ if (current === null || typeof current !== 'object' || Array.isArray(current) || !Object.hasOwn(current, segment.key)) {
1219
+ return { outcome: ProviderReportedFactReadOutcome.ABSENT };
1220
+ }
1221
+ current = current[segment.key];
1222
+ if (segment.select === undefined)
1223
+ continue;
1224
+ if (!Array.isArray(current))
1225
+ return { outcome: ProviderReportedFactReadOutcome.ABSENT };
1226
+ const { field, equals } = segment.select;
1227
+ const matches = current.filter((element) => element !== null && typeof element === 'object' && !Array.isArray(element)
1228
+ && Object.hasOwn(element, field) && providerReportedScalarText(element[field], true) === equals);
1229
+ if (matches.length !== 1) {
1230
+ return { outcome: ProviderReportedFactReadOutcome.SELECTION_NOT_SINGLE, segment: `${segment.key}[${field}=${equals}]`, matched: matches.length };
1231
+ }
1232
+ current = matches[0];
1233
+ }
1234
+ const value = providerReportedScalarText(current, false);
1235
+ return value === undefined ? { outcome: ProviderReportedFactReadOutcome.ABSENT } : { outcome: ProviderReportedFactReadOutcome.FOUND, value };
1236
+ }
1237
+ /**
1238
+ * The query parameters an authorization redirect carries for the OAuth protocol itself, never as an account fact: a
1239
+ * redirect-sourced fact named like one would read the code, the state or the error as the account.
1240
+ * spec-cite: RFC 6749 §4.1.2 (`code`, `state`) and §4.1.2.1 (`error`, `error_description`, `error_uri`); RFC 9207 §2
1241
+ * (`iss`).
1242
+ * semantic-literal-boundary: the OAuth protocol's own redirect parameter names, compared with a module's declaration.
1243
+ */
1244
+ export const PROVIDER_OAUTH2_REDIRECT_PROTOCOL_PARAMETERS = ['code', 'state', 'error', 'error_description', 'error_uri', 'iss'];
643
1245
  /** What a {@link ProviderOAuth2IdTokenRequirement} asks of the sign-in's ID token claims. */
644
1246
  export var ProviderOAuth2IdTokenRequirementKind;
645
1247
  (function (ProviderOAuth2IdTokenRequirementKind) {
@@ -753,6 +1355,118 @@ export function describeProviderOAuth2IdTokenRequirementDefects(oauth2) {
753
1355
  }
754
1356
  return defects;
755
1357
  }
1358
+ /**
1359
+ * Whether a vendor-reported value may stand as one path segment under its fact's declared
1360
+ * {@link ProviderOAuth2ReportedAccountFact.pathSegmentPattern} (#3397): the WHOLE value matches the pattern, and it is
1361
+ * not empty, not a dot-segment and holds no `/`, whatever the pattern admits. The one admission, applied where the value
1362
+ * is captured (the authorization) and where it is spent (every call). A pattern that does not compile admits nothing.
1363
+ */
1364
+ export function providerReportedPathSegmentAdmits(pattern, value) {
1365
+ if (value.length === 0 || value === '.' || value === '..' || value.includes('/'))
1366
+ return false;
1367
+ let expression;
1368
+ try {
1369
+ expression = new RegExp(`^(?:${pattern})$`, 'u');
1370
+ }
1371
+ catch {
1372
+ return false;
1373
+ }
1374
+ return expression.test(value);
1375
+ }
1376
+ /**
1377
+ * What makes the declared USES of a connection's vendor-reported facts unsafe or unusable (#3397), checked at boot with
1378
+ * the contract's other declarations:
1379
+ *
1380
+ * - a fact read from the authorization redirect is something the person who authorized could change, so it may not be
1381
+ * an `origin`, fill a base URL host, or route inbound deliveries, and its name must be one query parameter that is
1382
+ * not the OAuth protocol's own (`code`, `state`, …);
1383
+ * - a `pathSegmentPattern` must compile, admit no empty value, and not sit beside an `origin`;
1384
+ * - a `fillsBaseUrlHost` fact must name a `{placeholder}` of the base URL that a `DECLARED_VALUES` allowance governs,
1385
+ * must not also be an `origin` or something the person is asked at connect, and cannot share a contract with
1386
+ * `baseUrlConnectionFact`, which already decides where calls go;
1387
+ * - every other `responsePath` must parse ({@link parseProviderReportedFactPath});
1388
+ * - a fact read from the userinfo answer needs a `userInfoUrl`, a `userInfoUrl` needs a fact that reads it (a request
1389
+ * carrying the person's token that nothing uses is not made), and the userinfo endpoint may carry only the
1390
+ * host-forming placeholders the token endpoint carries, in the same position (it is the same authorization server).
1391
+ */
1392
+ export function describeProviderOAuth2ReportedFactUseDefects(contract) {
1393
+ const defects = [];
1394
+ for (const [name, fact] of Object.entries(contract.oauth2?.reportedAccountFacts ?? {})) {
1395
+ const at = `reported fact '${name}'`;
1396
+ if (fact.source === ProviderOAuth2ReportedFactSource.AUTHORIZATION_REDIRECT_QUERY) {
1397
+ if (fact.responsePath.length === 0 || fact.responsePath.includes('.') || PROVIDER_OAUTH2_REDIRECT_PROTOCOL_PARAMETERS.includes(fact.responsePath)) {
1398
+ defects.push(`${at} reads the redirect query parameter '${fact.responsePath}', which is not one parameter name outside the OAuth protocol's own`);
1399
+ }
1400
+ if (fact.origin !== undefined || fact.fillsBaseUrlHost === true || fact.routesInboundDeliveries === true) {
1401
+ defects.push(`${at} comes from the authorization redirect, which the person authorizing could alter, so it may not decide an origin, a host or delivery routing`);
1402
+ }
1403
+ if (fact.responsePath.includes('['))
1404
+ defects.push(`${at} selects inside the redirect query parameter '${fact.responsePath}', which is one string`);
1405
+ }
1406
+ else if (parseProviderReportedFactPath(fact.responsePath) === undefined) {
1407
+ defects.push(`${at} reads the responsePath ${JSON.stringify(fact.responsePath)}, which is not a dot path of names, each optionally selecting one element as name[field=value]`);
1408
+ }
1409
+ if (fact.source === ProviderOAuth2ReportedFactSource.USERINFO_RESPONSE && contract.oauth2?.userInfoUrl === undefined) {
1410
+ defects.push(`${at} reads the userinfo answer, and the OAuth2 contract declares no userInfoUrl`);
1411
+ }
1412
+ if (fact.pathSegmentPattern !== undefined) {
1413
+ let compiles = true;
1414
+ try {
1415
+ new RegExp(`^(?:${fact.pathSegmentPattern})$`, 'u');
1416
+ }
1417
+ catch {
1418
+ compiles = false;
1419
+ }
1420
+ if (!compiles)
1421
+ defects.push(`${at} declares the pathSegmentPattern ${JSON.stringify(fact.pathSegmentPattern)}, which does not compile`);
1422
+ else if (new RegExp(`^(?:${fact.pathSegmentPattern})$`, 'u').test(''))
1423
+ defects.push(`${at} declares a pathSegmentPattern that admits an empty value`);
1424
+ if (fact.origin !== undefined)
1425
+ defects.push(`${at} is both an origin and a path value; a fact addresses the account one way`);
1426
+ }
1427
+ if (fact.fillsBaseUrlHost === true) {
1428
+ if (fact.origin !== undefined)
1429
+ defects.push(`${at} is both an origin and a base URL host; a fact addresses the account one way`);
1430
+ if (contract.baseUrlConnectionFact !== undefined)
1431
+ defects.push(`${at} fills a base URL host beside baseUrlConnectionFact, and only one may decide where a call goes`);
1432
+ if (contract.baseUrl === undefined || !contract.baseUrl.includes(`{${name}}`))
1433
+ defects.push(`${at} fills a base URL host, and the base URL carries no '{${name}}' placeholder`);
1434
+ const allowance = contract.baseUrlHostAllowances !== undefined && Object.hasOwn(contract.baseUrlHostAllowances, name) ? contract.baseUrlHostAllowances[name] : undefined;
1435
+ if (allowance?.kind !== ProviderOAuth2EndpointHostAllowanceKind.DECLARED_VALUES) {
1436
+ defects.push(`${at} fills a base URL host that no DECLARED_VALUES baseUrlHostAllowances entry governs`);
1437
+ }
1438
+ if ((contract.oauth2?.connectionParameters ?? []).includes(name))
1439
+ defects.push(`${at} fills a base URL host the person is also asked for at connect`);
1440
+ }
1441
+ }
1442
+ const userInfoUrl = contract.oauth2?.userInfoUrl;
1443
+ if (userInfoUrl !== undefined) {
1444
+ const facts = Object.values(contract.oauth2?.reportedAccountFacts ?? {});
1445
+ if (!facts.some((fact) => fact.source === ProviderOAuth2ReportedFactSource.USERINFO_RESPONSE)) {
1446
+ defects.push('declares a userInfoUrl that no reported fact reads, so the person\'s token would be sent there for nothing');
1447
+ }
1448
+ const templateDefects = findProviderOAuth2EndpointTemplateDefects(userInfoUrl);
1449
+ if (templateDefects.length > 0 || !userInfoUrl.toLowerCase().startsWith('https://')) {
1450
+ defects.push(`declares the userInfoUrl '${userInfoUrl}', which is not an https endpoint template (${templateDefects.join(', ') || 'not https'})`);
1451
+ }
1452
+ else {
1453
+ const tokenPositions = new Map();
1454
+ if (contract.oauth2?.tokenUrl !== undefined && findProviderOAuth2EndpointTemplateDefects(contract.oauth2.tokenUrl).length === 0) {
1455
+ for (const occurrence of classifyProviderOAuth2EndpointPlaceholders(contract.oauth2.tokenUrl))
1456
+ tokenPositions.set(occurrence.name, occurrence.position);
1457
+ }
1458
+ for (const occurrence of classifyProviderOAuth2EndpointPlaceholders(userInfoUrl)) {
1459
+ if (occurrence.position === ProviderOAuth2EndpointPlaceholderPosition.PATH_OR_QUERY) {
1460
+ defects.push(`the userInfoUrl carries the path placeholder '{${occurrence.name}}'; it is one fixed endpoint of the authorization server`);
1461
+ }
1462
+ else if (tokenPositions.get(occurrence.name) !== occurrence.position) {
1463
+ defects.push(`the userInfoUrl's host placeholder '{${occurrence.name}}' is not the token endpoint's own, in the same position; both belong to one authorization server`);
1464
+ }
1465
+ }
1466
+ }
1467
+ }
1468
+ return defects;
1469
+ }
756
1470
  /** How a placeholder's value is compared with a declared one. */
757
1471
  export var ProviderOAuth2ValueComparison;
758
1472
  (function (ProviderOAuth2ValueComparison) {
@@ -993,6 +1707,91 @@ export var ProviderOAuth2EndpointTemplateDefect;
993
1707
  * hyphen; 63 octets per label).
994
1708
  */
995
1709
  export const PROVIDER_OAUTH2_DNS_NAME_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/;
1710
+ /**
1711
+ * Whether `host` is `<labels><suffix>`: it ends with the dotted `suffix`, what precedes the suffix is a DNS name (never
1712
+ * empty, so the bare suffix is not admitted), and that DNS name carries at most `maxLabels` labels (#3640).
1713
+ *
1714
+ * THE one suffix test. Every place a declared host suffix admits a host — an answer filled into an endpoint or a base URL,
1715
+ * an audience selecting a request host, the token-endpoint host description, a declined value a suffix would still admit —
1716
+ * asks this function, so a label bound declared once holds at all of them. `maxLabels` absent means unbounded, which is
1717
+ * what every suffix meant before the bound existed. A bound that is not a positive integer admits nothing (fail closed):
1718
+ * the declaration checks refuse it where a person is reading a diff, and this keeps the runtime closed regardless.
1719
+ *
1720
+ * Case-insensitive; the caller need not lower-case.
1721
+ */
1722
+ export function providerHostIsUnderSuffix(host, suffix, maxLabels) {
1723
+ const candidate = host.toLowerCase();
1724
+ const lowered = suffix.toLowerCase();
1725
+ if (!lowered.startsWith('.') || !candidate.endsWith(lowered) || candidate.length === lowered.length)
1726
+ return false;
1727
+ const labels = candidate.slice(0, candidate.length - lowered.length);
1728
+ if (!PROVIDER_OAUTH2_DNS_NAME_PATTERN.test(labels))
1729
+ return false;
1730
+ return maxLabels === undefined || labels.split('.').length <= maxLabels;
1731
+ }
1732
+ /**
1733
+ * Whether some host is admitted under BOTH suffixes (#3640). Two suffixes can only share a host when one ends with the
1734
+ * other (`.a.b` and `.b`): a host under the longer one carries the extra labels (`.a`) before the shorter one, so the
1735
+ * shorter one's bound must leave room for one label of its own plus those. Equal suffixes always share a host.
1736
+ */
1737
+ export function providerHostSuffixesOverlap(left, right) {
1738
+ const leftSuffix = left.suffix.toLowerCase();
1739
+ const rightSuffix = right.suffix.toLowerCase();
1740
+ const [longer, shorter] = leftSuffix.length >= rightSuffix.length ? [left, right] : [right, left];
1741
+ const longerSuffix = longer.suffix.toLowerCase();
1742
+ const shorterSuffix = shorter.suffix.toLowerCase();
1743
+ if (!longerSuffix.endsWith(shorterSuffix))
1744
+ return false;
1745
+ // The probe host: one label under the longer suffix, which is the fewest labels any host under it carries.
1746
+ return providerHostIsUnderSuffix(`a${longerSuffix}`, longerSuffix, longer.maxLabels) && providerHostIsUnderSuffix(`a${longerSuffix}`, shorterSuffix, shorter.maxLabels);
1747
+ }
1748
+ /**
1749
+ * The bare answer for a placeholder that OPENS a host, when the person typed the whole host (#3640).
1750
+ *
1751
+ * `https://{shop}.myshopify.com` asks for the store's handle, and the person connecting sees `acme.myshopify.com` in the
1752
+ * vendor's own screens. Filled verbatim, that answer dials `acme.myshopify.com.myshopify.com`: a host the suffix admits
1753
+ * (it ends in `.myshopify.com`) that does not exist, so every request fails on DNS naming a host rather than the answer.
1754
+ * The literal text the template writes after the placeholder (`literalHostSuffix`) is therefore stripped ONCE from an
1755
+ * answer that ends with it: `acme.myshopify.com` → `acme`. Nothing else is rewritten; the result is then admitted like
1756
+ * any other answer, so an answer with too many labels left (`acme.evil.myshopify.com` → `acme.evil` under a one-label
1757
+ * bound) is still refused.
1758
+ *
1759
+ * Generic rather than per vendor, because it is a property of the TEMPLATE: every provider whose host opens with a
1760
+ * placeholder (Shopify, Zendesk) asks for the part before a literal the vendor shows people with it attached. Done where
1761
+ * the template is filled, not in a connect prompt, so the OAuth start, the stored-answer re-admission and every
1762
+ * operation's base URL read the same answer the same way, whichever screen or flag supplied it. A host that literally
1763
+ * repeated its suffix is never a real vendor host, so nothing a person could mean is lost.
1764
+ */
1765
+ export function stripProviderHostAnswerTemplateSuffix(value, literalHostSuffix) {
1766
+ if (literalHostSuffix === undefined || literalHostSuffix === '')
1767
+ return value;
1768
+ return value.length > literalHostSuffix.length && value.toLowerCase().endsWith(literalHostSuffix.toLowerCase())
1769
+ ? value.slice(0, value.length - literalHostSuffix.length)
1770
+ : value;
1771
+ }
1772
+ /**
1773
+ * The words a refusal uses for the answer a bounded suffix expects (#3640): `a single DNS label before .myshopify.com
1774
+ * (the 'acme' of acme.myshopify.com)`. Shared so every refusal names the same form.
1775
+ */
1776
+ export function describeProviderHostSuffixExpectedForm(suffix, maxLabels, answerExcludesSuffix) {
1777
+ const count = maxLabels === undefined ? 'DNS labels' : maxLabels === 1 ? 'a single DNS label' : `at most ${maxLabels} DNS labels`;
1778
+ const example = answerExcludesSuffix ? `the 'acme' of acme${suffix}` : `acme${suffix}`;
1779
+ return `${count} before ${suffix} (${example})`;
1780
+ }
1781
+ /**
1782
+ * Why a declared label bound on host suffixes cannot be honoured (#3640): it is not a positive integer, or there is no
1783
+ * suffix for it to bound. Shared by the endpoint allowances and the audiences, so both refuse the same shapes.
1784
+ */
1785
+ export function describeProviderHostSuffixMaxLabelsDefects(maxLabels, hostSuffixes) {
1786
+ if (maxLabels === undefined)
1787
+ return [];
1788
+ const defects = [];
1789
+ if (!Number.isInteger(maxLabels) || maxLabels < 1)
1790
+ defects.push(`hostSuffixMaxLabels ${String(maxLabels)} is not a positive integer, which admits no host`);
1791
+ if ((hostSuffixes ?? []).length === 0)
1792
+ defects.push('hostSuffixMaxLabels is declared with no hostSuffixes to bound');
1793
+ return defects;
1794
+ }
996
1795
  /**
997
1796
  * Why a DECLARED host allowance cannot be applied where its placeholder sits — the position rules the
998
1797
  * transposer enforces at GENERATION time, so a broken declaration refuses the batch instead of being
@@ -1076,10 +1875,13 @@ export function describeProviderOAuth2EndpointHostAllowanceDefects(placeholder,
1076
1875
  defects.push(`host suffix '${suffix}' must begin with '.' and name at least two DNS labels (a bare '.com' admits every host under a TLD)`);
1077
1876
  }
1078
1877
  }
1878
+ defects.push(...describeProviderHostSuffixMaxLabelsDefects(allowance.hostSuffixMaxLabels, allowance.hostSuffixes));
1079
1879
  defects.push(...describeProviderOAuth2UnsupportedValueDefects(allowance.unsupportedValues, allowance.values, ProviderOAuth2ValueComparison.CASE_INSENSITIVE));
1080
1880
  for (const declined of Object.keys(allowance.unsupportedValues ?? {})) {
1081
1881
  // A declined value a declared SUFFIX would still admit is admitted, whatever the reason says.
1082
- const coveringSuffix = (allowance.hostSuffixes ?? []).find((suffix) => declined.toLowerCase().endsWith(suffix.toLowerCase()));
1882
+ // ORIGIN values carry the scheme; the suffix is matched against the host they name.
1883
+ const declinedHost = declined.toLowerCase().replace(/^https:\/\//, '');
1884
+ const coveringSuffix = (allowance.hostSuffixes ?? []).find((suffix) => providerHostIsUnderSuffix(declinedHost, suffix, allowance.hostSuffixMaxLabels));
1083
1885
  if (coveringSuffix !== undefined)
1084
1886
  defects.push(`'${declined}' is declared unsupported but the host suffix '${coveringSuffix}' admits it`);
1085
1887
  }