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

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 (59) hide show
  1. package/dist/esm/core/commons.external-connectors-models.schemas.d.ts +130 -0
  2. package/dist/esm/core/commons.external-connectors-models.schemas.d.ts.map +1 -1
  3. package/dist/esm/core/commons.external-connectors-models.schemas.js +193 -0
  4. package/dist/esm/core/commons.external-connectors-models.schemas.js.map +1 -1
  5. package/dist/esm/core/security.external-connectors-models.schemas.d.ts +2 -0
  6. package/dist/esm/core/security.external-connectors-models.schemas.d.ts.map +1 -1
  7. package/dist/esm/core/security.external-connectors-models.schemas.js +10 -4
  8. package/dist/esm/core/security.external-connectors-models.schemas.js.map +1 -1
  9. package/dist/esm/index.d.ts +2 -0
  10. package/dist/esm/index.d.ts.map +1 -1
  11. package/dist/esm/index.js +2 -0
  12. package/dist/esm/index.js.map +1 -1
  13. package/dist/esm/protocols/agent-client-protocol.external-connectors-models.schemas.d.ts +1 -0
  14. package/dist/esm/protocols/agent-client-protocol.external-connectors-models.schemas.d.ts.map +1 -1
  15. package/dist/esm/protocols/agent-client-protocol.external-connectors-models.schemas.js +10 -1
  16. package/dist/esm/protocols/agent-client-protocol.external-connectors-models.schemas.js.map +1 -1
  17. package/dist/esm/protocols/billing.standard-interface.schemas.d.ts +8 -0
  18. package/dist/esm/protocols/billing.standard-interface.schemas.d.ts.map +1 -1
  19. package/dist/esm/protocols/billing.standard-interface.schemas.js +8 -0
  20. package/dist/esm/protocols/billing.standard-interface.schemas.js.map +1 -1
  21. package/dist/esm/protocols/content-safety.external-connectors-models.schemas.d.ts +150 -0
  22. package/dist/esm/protocols/content-safety.external-connectors-models.schemas.d.ts.map +1 -0
  23. package/dist/esm/protocols/content-safety.external-connectors-models.schemas.js +182 -0
  24. package/dist/esm/protocols/content-safety.external-connectors-models.schemas.js.map +1 -0
  25. package/dist/esm/protocols/document-extraction.external-connectors-models.schemas.d.ts +41 -9
  26. package/dist/esm/protocols/document-extraction.external-connectors-models.schemas.d.ts.map +1 -1
  27. package/dist/esm/protocols/document-extraction.external-connectors-models.schemas.js +40 -14
  28. package/dist/esm/protocols/document-extraction.external-connectors-models.schemas.js.map +1 -1
  29. package/dist/esm/protocols/email.standard-interface.schemas.d.ts.map +1 -1
  30. package/dist/esm/protocols/email.standard-interface.schemas.js +4 -0
  31. package/dist/esm/protocols/email.standard-interface.schemas.js.map +1 -1
  32. package/dist/esm/protocols/embeddings.external-connectors-models.schemas.d.ts +37 -4
  33. package/dist/esm/protocols/embeddings.external-connectors-models.schemas.d.ts.map +1 -1
  34. package/dist/esm/protocols/embeddings.external-connectors-models.schemas.js +57 -1
  35. package/dist/esm/protocols/embeddings.external-connectors-models.schemas.js.map +1 -1
  36. package/dist/esm/protocols/file-storage.standard-interface.d.ts +6 -0
  37. package/dist/esm/protocols/file-storage.standard-interface.d.ts.map +1 -1
  38. package/dist/esm/protocols/file-storage.standard-interface.js.map +1 -1
  39. package/dist/esm/protocols/hooks-capability.external-connectors-models.schemas.d.ts +7 -2
  40. package/dist/esm/protocols/hooks-capability.external-connectors-models.schemas.d.ts.map +1 -1
  41. package/dist/esm/protocols/hooks-capability.external-connectors-models.schemas.js.map +1 -1
  42. package/dist/esm/protocols/hooks.external-connectors-models.schemas.d.ts +388 -7
  43. package/dist/esm/protocols/hooks.external-connectors-models.schemas.d.ts.map +1 -1
  44. package/dist/esm/protocols/hooks.external-connectors-models.schemas.js +238 -0
  45. package/dist/esm/protocols/hooks.external-connectors-models.schemas.js.map +1 -1
  46. package/dist/esm/protocols/llm.external-connectors-models.schemas.d.ts +70 -5
  47. package/dist/esm/protocols/llm.external-connectors-models.schemas.d.ts.map +1 -1
  48. package/dist/esm/protocols/llm.external-connectors-models.schemas.js +96 -5
  49. package/dist/esm/protocols/llm.external-connectors-models.schemas.js.map +1 -1
  50. package/dist/esm/protocols/rest-operations.external-connectors-models.schemas.d.ts +446 -5
  51. package/dist/esm/protocols/rest-operations.external-connectors-models.schemas.d.ts.map +1 -1
  52. package/dist/esm/protocols/rest-operations.external-connectors-models.schemas.js +406 -0
  53. package/dist/esm/protocols/rest-operations.external-connectors-models.schemas.js.map +1 -1
  54. package/dist/esm/protocols/subject-erasure-effect.standard-interface.d.ts +133 -0
  55. package/dist/esm/protocols/subject-erasure-effect.standard-interface.d.ts.map +1 -0
  56. package/dist/esm/protocols/subject-erasure-effect.standard-interface.js +61 -0
  57. package/dist/esm/protocols/subject-erasure-effect.standard-interface.js.map +1 -0
  58. package/dist/tsconfig.build.tsbuildinfo +1 -1
  59. package/package.json +2 -2
@@ -88,6 +88,13 @@ export var ProviderRestResponseDecoding;
88
88
  ProviderRestResponseDecoding["TEXT"] = "text";
89
89
  /** Read nothing — a write whose answer carries no information (a 204). The output is `undefined`. */
90
90
  ProviderRestResponseDecoding["NONE"] = "none";
91
+ /**
92
+ * Keep the raw bytes — a signed PDF, an evidence container (#2910). Requires `binaryResponse` (a byte ceiling and the
93
+ * accepted media types); the output is `{ bytes, contentType }`. The bytes are the vendor's, unscanned: do not relay
94
+ * them to a person or write them as a file yourself. Keep one through `ProviderDocumentFileWriterBackendService`
95
+ * (`@wildo-ai/saas-backend-lib`, #2963), which stores it as a scanned file (`scan: REQUIRED`) whatever the caller asks.
96
+ */
97
+ ProviderRestResponseDecoding["BINARY"] = "binary";
91
98
  })(ProviderRestResponseDecoding || (ProviderRestResponseDecoding = {}));
92
99
  /**
93
100
  * How a listing operation continues. A HINT the caller reads, never something the executor acts
@@ -102,6 +109,152 @@ export var ProviderRestPaginationStyle;
102
109
  /** The vendor returns its own continuation URL (`@odata.nextLink`, `links.next`). */
103
110
  ProviderRestPaginationStyle["LINK"] = "link";
104
111
  })(ProviderRestPaginationStyle || (ProviderRestPaginationStyle = {}));
112
+ /** What a successful HTTP answer said about the call, read through a {@link ProviderRestApplicationOutcome}. */
113
+ export var ProviderRestApplicationOutcomeKind;
114
+ (function (ProviderRestApplicationOutcomeKind) {
115
+ /** The success flag is `true`. */
116
+ ProviderRestApplicationOutcomeKind["SUCCEEDED"] = "succeeded";
117
+ /** The success flag is `false`: the vendor refused the call, naming why at the error path when it could. */
118
+ ProviderRestApplicationOutcomeKind["REFUSED"] = "refused";
119
+ /** The flag is absent or not a boolean: the answer does not carry the envelope its provider declares. */
120
+ ProviderRestApplicationOutcomeKind["UNSTATED"] = "unstated";
121
+ })(ProviderRestApplicationOutcomeKind || (ProviderRestApplicationOutcomeKind = {}));
122
+ /**
123
+ * A vendor error code as the engine may carry it: identifier characters only, at most 128. A value of any other shape
124
+ * is dropped rather than carried, because it travels to audit rows, logs and API answers, and a vendor that puts a
125
+ * sentence (or request data) where a code belongs must not have it repeated there.
126
+ */
127
+ export const PROVIDER_VENDOR_ERROR_CODE_SHAPE = /^[A-Za-z0-9_.:-]{1,128}$/;
128
+ /**
129
+ * The defects of a declared outcome envelope, refused at boot: an empty path, and a credential-refusal code that is not
130
+ * code-shaped, which the transport would never carry and so would never match (#2661 review L7).
131
+ */
132
+ export function describeProviderRestApplicationOutcomeDefects(outcome) {
133
+ const defects = [];
134
+ const path = /^[A-Za-z0-9_]+(\.[A-Za-z0-9_]+)*$/;
135
+ if (!path.test(outcome.successFlagPath))
136
+ defects.push(`applicationOutcome.successFlagPath '${outcome.successFlagPath}' is not a dot-path`);
137
+ if (!path.test(outcome.errorCodePath))
138
+ defects.push(`applicationOutcome.errorCodePath '${outcome.errorCodePath}' is not a dot-path`);
139
+ for (const code of outcome.credentialRefusedErrorCodes ?? []) {
140
+ if (!PROVIDER_VENDOR_ERROR_CODE_SHAPE.test(code)) {
141
+ defects.push(`applicationOutcome.credentialRefusedErrorCodes names '${code}', which is not code-shaped and would never match a code the transport keeps`);
142
+ }
143
+ }
144
+ return defects;
145
+ }
146
+ /**
147
+ * The placeholder a client-credentials token URL may carry for the tenant the credential belongs to:
148
+ * `https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token`. Filled from the connection's
149
+ * {@link ProviderRestClientCredentials.tenantSecretRef} slot, never from a request.
150
+ */
151
+ export const PROVIDER_CLIENT_CREDENTIALS_TENANT_PLACEHOLDER = '{tenant}';
152
+ /** A tenant value the token URL admits: a GUID or a DNS name. Nothing that can leave the URL's path segment. */
153
+ export const PROVIDER_CLIENT_CREDENTIALS_TENANT_SHAPE = /^[A-Za-z0-9](?:[A-Za-z0-9.-]{0,251}[A-Za-z0-9])?$/;
154
+ /** What a {@link ProviderClientCredentialsVerifiedFact} records: a value the person typed and the vendor accepted. */
155
+ export var ProviderClientCredentialsFactSource;
156
+ (function (ProviderClientCredentialsFactSource) {
157
+ /** The client id the token endpoint issued a token for: Microsoft's bot App ID. */
158
+ ProviderClientCredentialsFactSource["CLIENT_ID"] = "client_id";
159
+ /** The tenant whose token endpoint issued it. */
160
+ ProviderClientCredentialsFactSource["TENANT"] = "tenant";
161
+ })(ProviderClientCredentialsFactSource || (ProviderClientCredentialsFactSource = {}));
162
+ /**
163
+ * Why a client-credentials declaration cannot be honoured, or an empty list. Checked where a provider is composed:
164
+ * 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
165
+ * with no tenant slot (or the reverse), or any other placeholder in the URL.
166
+ */
167
+ export function describeProviderRestClientCredentialsDefects(credentials) {
168
+ const defects = [];
169
+ const hasTenant = credentials.tokenUrl.includes(PROVIDER_CLIENT_CREDENTIALS_TENANT_PLACEHOLDER);
170
+ const filled = credentials.tokenUrl.split(PROVIDER_CLIENT_CREDENTIALS_TENANT_PLACEHOLDER).join('tenant.example');
171
+ if (/[{}]/.test(filled))
172
+ defects.push(`clientCredentials.tokenUrl '${credentials.tokenUrl}' carries a placeholder other than ${PROVIDER_CLIENT_CREDENTIALS_TENANT_PLACEHOLDER}`);
173
+ if (hasTenant && credentials.tenantSecretRef === undefined)
174
+ defects.push(`clientCredentials.tokenUrl carries ${PROVIDER_CLIENT_CREDENTIALS_TENANT_PLACEHOLDER} and no tenantSecretRef fills it`);
175
+ if (!hasTenant && credentials.tenantSecretRef !== undefined)
176
+ defects.push(`clientCredentials.tenantSecretRef '${credentials.tenantSecretRef}' fills nothing: the token URL carries no ${PROVIDER_CLIENT_CREDENTIALS_TENANT_PLACEHOLDER}`);
177
+ const host = /^https:\/\/([^/:?#]+)(?::\d+)?\//.exec(filled)?.[1]?.toLowerCase();
178
+ if (host === undefined)
179
+ 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`);
185
+ }
186
+ if (host !== undefined && !credentials.tokenHosts.some((tokenHost) => tokenHost.toLowerCase() === host)) {
187
+ defects.push(`clientCredentials.tokenUrl is on '${host}', which tokenHosts does not admit`);
188
+ }
189
+ if (credentials.scope.trim() === '')
190
+ defects.push('clientCredentials.scope is empty');
191
+ for (const [name, fact] of Object.entries(credentials.verifiedAccountFacts ?? {})) {
192
+ if (!/^[A-Za-z][A-Za-z0-9_]{0,63}$/.test(name))
193
+ defects.push(`clientCredentials.verifiedAccountFacts names '${name}', which is not a fact name`);
194
+ if (fact.source === ProviderClientCredentialsFactSource.TENANT && credentials.tenantSecretRef === undefined) {
195
+ defects.push(`clientCredentials.verifiedAccountFacts '${name}' records the tenant, and no tenantSecretRef holds one`);
196
+ }
197
+ }
198
+ if (credentials.tokenLifetimeFallbackSeconds !== undefined && !(credentials.tokenLifetimeFallbackSeconds > 0)) {
199
+ defects.push('clientCredentials.tokenLifetimeFallbackSeconds is not a positive number');
200
+ }
201
+ return defects;
202
+ }
203
+ function readDotPath(body, path) {
204
+ let current = body;
205
+ for (const segment of path.split('.')) {
206
+ if (current === null || typeof current !== 'object' || !Object.hasOwn(current, segment))
207
+ return undefined;
208
+ current = current[segment];
209
+ }
210
+ return current;
211
+ }
212
+ /**
213
+ * The vendor's error code at `errorCodePath` in a parsed answer, when it is code-shaped. Used for a refused 2xx and for
214
+ * a non-2xx answer alike: a vendor that states its code in the body states it in the same place whatever the status.
215
+ */
216
+ export function readProviderVendorErrorCode(body, errorCodePath) {
217
+ const code = readDotPath(body, errorCodePath);
218
+ return typeof code === 'string' && PROVIDER_VENDOR_ERROR_CODE_SHAPE.test(code) ? code : undefined;
219
+ }
220
+ /** Reads the verdict a parsed JSON answer carries. The ONE reader: the transport and every test go through it. */
221
+ export function readProviderRestApplicationOutcome(body, outcome) {
222
+ const flag = readDotPath(body, outcome.successFlagPath);
223
+ if (flag === true)
224
+ return { kind: ProviderRestApplicationOutcomeKind.SUCCEEDED };
225
+ if (flag !== false)
226
+ return { kind: ProviderRestApplicationOutcomeKind.UNSTATED };
227
+ const errorCode = readProviderVendorErrorCode(body, outcome.errorCodePath);
228
+ return errorCode === undefined
229
+ ? { kind: ProviderRestApplicationOutcomeKind.REFUSED }
230
+ : { kind: ProviderRestApplicationOutcomeKind.REFUSED, errorCode };
231
+ }
232
+ /**
233
+ * The QUERY parameters an operation declares under a name its provider places a credential in, compared IGNORING CASE
234
+ * (#2123, #2707). Empty when there is none.
235
+ *
236
+ * Such a parameter asks the caller for the credential the contract already places, so the operation would send the key
237
+ * twice, or send the caller's value instead of the configured one. A name differing only in case travels BESIDE the
238
+ * credential, and a vendor that reads names without regard to case may read the caller's value. Both are a declaration
239
+ * defect.
240
+ *
241
+ * ONE comparison for every place that refuses it, so they cannot drift apart:
242
+ * - the operation executor, once per catalogue load, against the contract's `apiKey.queryParameterName`;
243
+ * - the operation request builder, on every call, against the resolved access's query-placed names (the backstop for a
244
+ * placement that does not come from the contract);
245
+ * - `wildo` registry module checks, at publish, against the module's own contract.
246
+ *
247
+ * A PATH or HEADER parameter is not compared: a path token is a resource id that may merely be spelled like a
248
+ * credential, and a header credential is refused by its own placement rules.
249
+ */
250
+ export function providerRestOperationCredentialQueryParameters(operation, credentialQueryParameterNames) {
251
+ const credentialNames = new Set([...credentialQueryParameterNames].map((name) => name.toLowerCase()));
252
+ if (credentialNames.size === 0)
253
+ return [];
254
+ return (operation.parameters ?? [])
255
+ .filter((parameter) => parameter.in === ProviderRestParameterLocation.QUERY && credentialNames.has(parameter.name.toLowerCase()))
256
+ .map((parameter) => parameter.name);
257
+ }
105
258
  /** @wildo_source:part:start external-connectors.models.connection-rate-budgets facet:layer:shared facet:family:provider facet:audience:app-developer */
106
259
  /**
107
260
  * WHOSE allowance a vendor counts a request against, under one connection (#1729).
@@ -179,6 +332,24 @@ export function selectProviderRateBudgetDeclarations(input) {
179
332
  /** @wildo_source:part:start external-connectors.models.oauth2-audiences facet:layer:shared facet:family:provider facet:audience:app-developer */
180
333
  /** The placeholder a {@link ProviderRestOAuth2Audience.mintScopes} entry uses for the https origin of the request's host. */
181
334
  export const PROVIDER_OAUTH2_AUDIENCE_ORIGIN_PLACEHOLDER = '{audienceOrigin}';
335
+ /** What a vendor's URI scopes name, which decides the audience a scope belongs to — see {@link ProviderRestOAuth2.scopeMeaning}. */
336
+ export var ProviderOAuth2ScopeMeaning;
337
+ (function (ProviderOAuth2ScopeMeaning) {
338
+ /**
339
+ * A URI scope names the RESOURCE a token is minted for: the scope without its last path segment
340
+ * (`https://graph.microsoft.com/Mail.Send` is Graph's). Microsoft's model, and the default.
341
+ */
342
+ ProviderOAuth2ScopeMeaning["RESOURCE"] = "RESOURCE";
343
+ /**
344
+ * A URI scope names a PERMISSION of the vendor's one grant, not a resource: Google's scopes are URIs on several hosts
345
+ * (`https://www.googleapis.com/auth/gmail.readonly`, `https://mail.google.com/`, `https://spreadsheets.google.com/feeds`)
346
+ * and every one is consented on the same grant, whose one token serves every Google API. So every scope belongs to the
347
+ * PRIMARY audience, whatever host its URI names. Declared with a single audience only.
348
+ * spec-cite: https://developers.google.com/identity/protocols/oauth2/scopes — the scope tables, which list both forms
349
+ * (fetched 2026-10-05).
350
+ */
351
+ ProviderOAuth2ScopeMeaning["PERMISSION"] = "PERMISSION";
352
+ })(ProviderOAuth2ScopeMeaning || (ProviderOAuth2ScopeMeaning = {}));
182
353
  /** Whether `host` is admitted by one audience's hosts: an exact value, or `<labels><suffix>` for a declared suffix. */
183
354
  export function providerOAuth2AudienceAdmitsHost(audience, host) {
184
355
  const candidate = host.toLowerCase();
@@ -202,6 +373,10 @@ export function selectProviderOAuth2AudienceForHost(oauth2, host) {
202
373
  }
203
374
  return undefined;
204
375
  }
376
+ /** Whether `audience` mints its token per request ORIGIN (a mint scope beginning {@link PROVIDER_OAUTH2_AUDIENCE_ORIGIN_PLACEHOLDER}). */
377
+ export function providerOAuth2AudienceIsOriginScoped(audience) {
378
+ return audience.mintScopes.some((scope) => scope.startsWith(PROVIDER_OAUTH2_AUDIENCE_ORIGIN_PLACEHOLDER));
379
+ }
205
380
  /**
206
381
  * The scopes that mint `audience`'s token for a request to `origin` — `{audienceOrigin}` replaced by that origin.
207
382
  * The caller has already admitted the origin's host through {@link selectProviderOAuth2AudienceForHost}; nothing a
@@ -229,6 +404,9 @@ export function findProviderOAuth2ScopeAudience(oauth2, scope) {
229
404
  const audiences = oauth2.audiences ?? {};
230
405
  if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(scope))
231
406
  return oauth2.primaryAudience;
407
+ // A vendor whose scopes name permissions of one grant (Google): every scope is the primary audience's (#2546).
408
+ if (oauth2.scopeMeaning === ProviderOAuth2ScopeMeaning.PERMISSION)
409
+ return oauth2.primaryAudience;
232
410
  // The https origin's host, read by the same shape the audiences declare (a DNS name, no port, no credentials) rather
233
411
  // than by a URL parser: this package runs in a browser-neutral program with no URL global, and a scope that is not
234
412
  // that shape names no host an audience could admit.
@@ -262,6 +440,39 @@ export function providerOAuth2ScopeNamesDeclaredAudience(oauth2, scope) {
262
440
  return true;
263
441
  return findProviderOAuth2ScopeAudience(oauth2, scope) !== undefined;
264
442
  }
443
+ /**
444
+ * Why an operation's {@link ProviderRestOperation.host} cannot be used on a provider whose REST binding is `contract`, or
445
+ * `undefined` when it can (#2546). The token a call carries is chosen by the base URL's host, so the operation's host
446
+ * must belong to that same audience: then the same token is valid there and nothing is minted for it.
447
+ */
448
+ export function describeProviderRestOperationHostDefect(host, contract) {
449
+ if (host !== host.toLowerCase() || !PROVIDER_OAUTH2_DNS_NAME_PATTERN.test(host)) {
450
+ return `host '${host}' is not a lower-case DNS name (no scheme, port, path or placeholder)`;
451
+ }
452
+ if (contract.oauth2?.audiences === undefined) {
453
+ return `host '${host}' is declared on a provider whose connection declares no OAuth2 audiences, so its credential is valid only on the base URL's own host`;
454
+ }
455
+ // The base URL's https host, read without a URL parser (this package carries no DOM or Node types): the authority up to
456
+ // the first '/', '?' or '#'. Anything that is not a plain DNS name there (a placeholder, userinfo, a port) has no fixed
457
+ // audience and is refused.
458
+ const authority = contract.baseUrl === undefined ? undefined : /^https:\/\/([^/?#]*)/i.exec(contract.baseUrl)?.[1]?.toLowerCase();
459
+ const baseHost = authority !== undefined && PROVIDER_OAUTH2_DNS_NAME_PATTERN.test(authority) ? authority : undefined;
460
+ if (baseHost === undefined)
461
+ return `host '${host}' is declared on a provider whose base URL is not one fixed https URL, so the audience its token belongs to is not known`;
462
+ const baseAudience = selectProviderOAuth2AudienceForHost(contract.oauth2, baseHost);
463
+ const hostAudience = selectProviderOAuth2AudienceForHost(contract.oauth2, host);
464
+ if (hostAudience === undefined)
465
+ return `host '${host}' is admitted by none of the connection's audiences (${Object.keys(contract.oauth2.audiences).join(', ')})`;
466
+ if (baseAudience?.id !== hostAudience.id) {
467
+ return `host '${host}' belongs to the audience '${hostAudience.id}', not to '${baseAudience?.id ?? '(none)'}' that the base URL's token is for; an operation on another audience is a provider of its own sharing the connection`;
468
+ }
469
+ // An audience whose token is minted per ORIGIN (`{audienceOrigin}/.default`, a Dataverse environment) gives the base
470
+ // URL's origin its own token, which is not valid on another origin the same audience admits.
471
+ if (host !== baseHost && providerOAuth2AudienceIsOriginScoped(hostAudience.audience)) {
472
+ return `host '${host}' is another origin of the audience '${hostAudience.id}', whose token is minted for one origin at a time (the base URL's)`;
473
+ }
474
+ return undefined;
475
+ }
265
476
  /** @wildo_source:part:end external-connectors.models.oauth2-audiences */
266
477
  /** @wildo_source:part:start external-connectors.models.oauth2-incremental-consent facet:layer:shared facet:family:provider facet:audience:app-developer */
267
478
  /**
@@ -286,6 +497,69 @@ export var ProviderOAuth2IncrementalConsentForm;
286
497
  */
287
498
  ProviderOAuth2IncrementalConsentForm["INCLUDE_GRANTED_SCOPES"] = "INCLUDE_GRANTED_SCOPES";
288
499
  })(ProviderOAuth2IncrementalConsentForm || (ProviderOAuth2IncrementalConsentForm = {}));
500
+ /**
501
+ * How a vendor is asked for a refresh token, when a scope does not do it — see {@link ProviderRestOAuth2.offlineAccess}.
502
+ */
503
+ export var ProviderOAuth2OfflineAccessForm;
504
+ (function (ProviderOAuth2OfflineAccessForm) {
505
+ /**
506
+ * Google: the authorize request carries `access_type=offline`, without which no refresh token is issued. The parameter
507
+ * "Indicates whether your application can refresh access tokens when the user is not present at the browser. Valid
508
+ * parameter values are `online`, which is the default value, and `offline`."; and "the refresh token is only returned
509
+ * if your application set the `access_type` parameter to `offline`".
510
+ * spec-cite: https://developers.google.com/identity/protocols/oauth2/web-server — Step 1 (`access_type` row) and
511
+ * Step 5, fetched 2026-10-05.
512
+ *
513
+ * Google issues the refresh token at the FIRST exchange for a consent, so a request that does not grow an existing grant
514
+ * (a first connect, or a reconnect replacing a grant) also carries `prompt=consent`: a reconnect of an account that had
515
+ * already consented would otherwise store a grant with no refresh token, which expires within the hour. A request that
516
+ * grows a grant (`include_granted_scopes`) does not need it, because the stored grant keeps its refresh token.
517
+ * Decided by practice: the established Google integrations ask for `access_type=offline` with `prompt=consent` for
518
+ * exactly this reason.
519
+ */
520
+ ProviderOAuth2OfflineAccessForm["ACCESS_TYPE_OFFLINE"] = "ACCESS_TYPE_OFFLINE";
521
+ })(ProviderOAuth2OfflineAccessForm || (ProviderOAuth2OfflineAccessForm = {}));
522
+ /** Whether a vendor takes PKCE from a confidential client. See {@link ProviderRestOAuth2.pkce}. */
523
+ export var ProviderOAuth2PkceSupport;
524
+ (function (ProviderOAuth2PkceSupport) {
525
+ /** An S256 challenge on the authorize request and its verifier on the exchange: the default. */
526
+ ProviderOAuth2PkceSupport["S256"] = "S256";
527
+ /** The vendor refuses PKCE from a confidential client (Slack): neither is sent. */
528
+ ProviderOAuth2PkceSupport["NOT_ACCEPTED"] = "NOT_ACCEPTED";
529
+ })(ProviderOAuth2PkceSupport || (ProviderOAuth2PkceSupport = {}));
530
+ /**
531
+ * A `scope` list encoding that deviates from RFC 6749's space-delimited list, in either direction: the token answer
532
+ * ({@link ProviderRestOAuth2.grantedScopesDelimiter}) or the authorize request ({@link ProviderRestOAuth2.authorizeScopesDelimiter}).
533
+ * One vocabulary for both, because it is one question (how a vendor writes a list of scopes) asked of two messages.
534
+ */
535
+ export var ProviderOAuth2ScopeListDelimiter;
536
+ (function (ProviderOAuth2ScopeListDelimiter) {
537
+ /**
538
+ * GitHub's token answer is `{"access_token":"gho_…","scope":"repo,gist","token_type":"bearer"}`, while its authorize
539
+ * request takes "A space-delimited list of scopes". Slack's authorize URL takes "a comma-separated list of scopes, such
540
+ * as: `scope=incoming-webhook,commands`".
541
+ * spec-cite: https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps — step 2's JSON
542
+ * response example and step 1's `scope` row, fetched 2026-10-05; https://docs.slack.dev/authentication/installing-with-oauth
543
+ * — the authorize step's `scope` parameter, fetched 2026-10-06.
544
+ */
545
+ ProviderOAuth2ScopeListDelimiter["COMMA"] = "COMMA";
546
+ })(ProviderOAuth2ScopeListDelimiter || (ProviderOAuth2ScopeListDelimiter = {}));
547
+ /**
548
+ * The granted scopes a token answer's `scope` string names, read with the vendor's declared delimiter: whitespace (RFC
549
+ * 6749 §5.1) when none is declared, a comma for {@link ProviderOAuth2ScopeListDelimiter.COMMA}.
550
+ */
551
+ export function splitProviderOAuth2GrantedScopes(scope, delimiter) {
552
+ const separator = delimiter === ProviderOAuth2ScopeListDelimiter.COMMA ? /\s*,\s*/ : /\s+/;
553
+ return scope.split(separator).map((token) => token.trim()).filter((token) => token.length > 0);
554
+ }
555
+ /**
556
+ * The authorize request's `scope` value for the scopes asked for, written with the vendor's declared delimiter: a single
557
+ * space (RFC 6749 §3.3) when none is declared, a comma for {@link ProviderOAuth2ScopeListDelimiter.COMMA}.
558
+ * spec-cite: RFC 6749 §3.3 (scope is expressed as a list of space-delimited, case-sensitive strings).
559
+ */
560
+ export function joinProviderOAuth2RequestedScopes(scopes, delimiter) {
561
+ return scopes.join(delimiter === ProviderOAuth2ScopeListDelimiter.COMMA ? ',' : ' ');
562
+ }
289
563
  /** @wildo_source:part:end external-connectors.models.oauth2-incremental-consent */
290
564
  /**
291
565
  * Why a declared audience set cannot be honoured, or an empty list. Checked where a provider is composed, so a
@@ -302,6 +576,10 @@ export function describeProviderOAuth2AudienceDefects(oauth2) {
302
576
  if (audiences === undefined) {
303
577
  return oauth2.primaryAudience === undefined ? [] : [`primaryAudience '${oauth2.primaryAudience}' is declared with no audiences`];
304
578
  }
579
+ // Under PERMISSION every scope is the primary audience's, so a second audience could never be asked for (#2546).
580
+ if (oauth2.scopeMeaning === ProviderOAuth2ScopeMeaning.PERMISSION && Object.keys(audiences).length > 1) {
581
+ return [`scopeMeaning PERMISSION reads every scope as the primary audience's, so it is declared with one audience; this declares ${Object.keys(audiences).length}`];
582
+ }
305
583
  const defects = [];
306
584
  const primary = oauth2.primaryAudience === undefined ? undefined : audiences[oauth2.primaryAudience];
307
585
  if (primary === undefined)
@@ -347,6 +625,134 @@ export function describeProviderOAuth2AudienceDefects(oauth2) {
347
625
  }
348
626
  return defects;
349
627
  }
628
+ /** Where in the token endpoint's answer a {@link ProviderOAuth2ReportedAccountFact} is read. */
629
+ export var ProviderOAuth2ReportedFactSource;
630
+ (function (ProviderOAuth2ReportedFactSource) {
631
+ /** The token endpoint's JSON answer itself: Slack's `team.id`, Salesforce's `instance_url`. The default. */
632
+ ProviderOAuth2ReportedFactSource["TOKEN_RESPONSE"] = "token_response";
633
+ /**
634
+ * The claims of the `id_token` that answer carries (OpenID Connect): Microsoft's `tid`, the tenant the person signed
635
+ * in to. Read without checking the token's signature, which the specification permits for an ID token received
636
+ * directly from the token endpoint, because TLS has already authenticated who sent it. An answer carrying no
637
+ * `id_token` reports no such fact. This is how a vendor's account identity becomes VENDOR-REPORTED when its token
638
+ * answer names the account only inside the ID token (#2660).
639
+ * spec-cite: OpenID Connect Core 1.0 §3.1.3.7, item 6.
640
+ */
641
+ ProviderOAuth2ReportedFactSource["ID_TOKEN_CLAIMS"] = "id_token_claims";
642
+ })(ProviderOAuth2ReportedFactSource || (ProviderOAuth2ReportedFactSource = {}));
643
+ /** What a {@link ProviderOAuth2IdTokenRequirement} asks of the sign-in's ID token claims. */
644
+ export var ProviderOAuth2IdTokenRequirementKind;
645
+ (function (ProviderOAuth2IdTokenRequirementKind) {
646
+ /**
647
+ * The claim is an array holding at least one of `values`, compared exactly. Microsoft's `wids`, the directory roles
648
+ * the person holds: a tenant claim requires a role that administers the tenant.
649
+ */
650
+ ProviderOAuth2IdTokenRequirementKind["CLAIM_CONTAINS_ANY"] = "claim_contains_any";
651
+ /**
652
+ * When the claim is present, it equals `equalsClaim`. Microsoft's `idp` against `iss`: they differ for a guest, an
653
+ * account another tenant authenticated, who speaks for nothing in this one.
654
+ */
655
+ ProviderOAuth2IdTokenRequirementKind["CLAIM_EQUALS_CLAIM_WHEN_PRESENT"] = "claim_equals_claim_when_present";
656
+ })(ProviderOAuth2IdTokenRequirementKind || (ProviderOAuth2IdTokenRequirementKind = {}));
657
+ /** Why an ID token's claims may not be used (#2663 re-review I1). See {@link providerOAuth2IdTokenRejection}. */
658
+ export var ProviderOAuth2IdTokenRejection;
659
+ (function (ProviderOAuth2IdTokenRejection) {
660
+ /** The token endpoint's answer carries no ID token, or not one whose payload is a JSON object. */
661
+ ProviderOAuth2IdTokenRejection["ABSENT"] = "absent";
662
+ /** `iss` is not the declared issuer, with its placeholders filled from the token's claims. */
663
+ ProviderOAuth2IdTokenRejection["ISSUER_MISMATCH"] = "issuer_mismatch";
664
+ /** `aud` does not name this client, or names another audience beside it. */
665
+ ProviderOAuth2IdTokenRejection["AUDIENCE_MISMATCH"] = "audience_mismatch";
666
+ /** `exp` is missing, or not after the current time. */
667
+ ProviderOAuth2IdTokenRejection["EXPIRED"] = "expired";
668
+ })(ProviderOAuth2IdTokenRejection || (ProviderOAuth2IdTokenRejection = {}));
669
+ /**
670
+ * Why these ID token claims may not be used, or `undefined` when they may: the token names the declared issuer, this
671
+ * client as its only audience, and an expiry still ahead. See {@link ProviderRestOAuth2.idTokenIssuer}.
672
+ *
673
+ * A placeholder of the issuer naming a claim the token does not carry as a non-empty string fails the issuer check:
674
+ * an issuer that cannot be written cannot be matched.
675
+ */
676
+ export function providerOAuth2IdTokenRejection(claims, expected) {
677
+ if (claims === undefined)
678
+ return ProviderOAuth2IdTokenRejection.ABSENT;
679
+ const claimOf = (name) => (Object.hasOwn(claims, name) ? claims[name] : undefined);
680
+ let unfillable = false;
681
+ const issuer = expected.issuer.replace(/\{([^{}]+)\}/g, (_placeholder, name) => {
682
+ const value = claimOf(name);
683
+ if (typeof value !== 'string' || value.length === 0) {
684
+ unfillable = true;
685
+ return '';
686
+ }
687
+ return value;
688
+ });
689
+ if (unfillable || claimOf('iss') !== issuer)
690
+ return ProviderOAuth2IdTokenRejection.ISSUER_MISMATCH;
691
+ const audience = claimOf('aud');
692
+ const audiences = Array.isArray(audience) ? audience : [audience];
693
+ // Every listed audience must be this client: OpenID Connect rejects a token that lists "additional audiences not
694
+ // trusted by the Client", and this client trusts none but itself (no declaration names another).
695
+ if (audiences.length === 0 || !audiences.every((entry) => entry === expected.clientId))
696
+ return ProviderOAuth2IdTokenRejection.AUDIENCE_MISMATCH;
697
+ const expiry = claimOf('exp');
698
+ if (typeof expiry !== 'number' || !Number.isFinite(expiry) || expiry <= expected.nowEpochSeconds)
699
+ return ProviderOAuth2IdTokenRejection.EXPIRED;
700
+ return undefined;
701
+ }
702
+ /** Whether ID token claims meet one requirement. An absent ID token meets none. */
703
+ export function providerOAuth2IdTokenRequirementHolds(requirement, claims) {
704
+ if (claims === undefined)
705
+ return false;
706
+ const value = Object.hasOwn(claims, requirement.claim) ? claims[requirement.claim] : undefined;
707
+ switch (requirement.kind) {
708
+ case ProviderOAuth2IdTokenRequirementKind.CLAIM_CONTAINS_ANY:
709
+ return Array.isArray(value) && value.some((entry) => typeof entry === 'string' && requirement.values.includes(entry));
710
+ case ProviderOAuth2IdTokenRequirementKind.CLAIM_EQUALS_CLAIM_WHEN_PRESENT: {
711
+ if (value === undefined)
712
+ return true;
713
+ const other = Object.hasOwn(claims, requirement.equalsClaim) ? claims[requirement.equalsClaim] : undefined;
714
+ return typeof value === 'string' && value === other;
715
+ }
716
+ default: {
717
+ const unhandled = requirement;
718
+ return unhandled;
719
+ }
720
+ }
721
+ }
722
+ /**
723
+ * What makes a declaration of ID token requirements unusable: a requirement with no claim or no value, and any
724
+ * requirement on a contract that does not ask for `openid`, without which "the behavior is entirely unspecified" and
725
+ * no ID token is returned to read.
726
+ * spec-cite: OpenID Connect Core 1.0 §3.1.2.1 — the `scope` parameter row ("If the openid scope value is not present,
727
+ * the behavior is entirely unspecified").
728
+ */
729
+ export function describeProviderOAuth2IdTokenRequirementDefects(oauth2) {
730
+ const requirements = oauth2.idTokenRequirements ?? [];
731
+ const readsIdTokenFacts = Object.values(oauth2.reportedAccountFacts ?? {}).some((fact) => fact.source === ProviderOAuth2ReportedFactSource.ID_TOKEN_CLAIMS);
732
+ if (requirements.length === 0 && !readsIdTokenFacts)
733
+ return [];
734
+ const defects = [];
735
+ if (!(oauth2.scopes ?? []).includes('openid'))
736
+ defects.push('reads ID token claims without asking for the `openid` scope, so no ID token is returned to read them from');
737
+ // OpenID Connect Core §3.1.3.7 item 2: a claim is used only from a token naming the expected issuer.
738
+ if (oauth2.idTokenIssuer === undefined || oauth2.idTokenIssuer.trim().length === 0) {
739
+ defects.push('reads ID token claims without declaring `idTokenIssuer`, the issuer a token must name before any of its claims is used');
740
+ }
741
+ else if (!oauth2.idTokenIssuer.startsWith('https://')) {
742
+ defects.push(`declares \`idTokenIssuer\` ${JSON.stringify(oauth2.idTokenIssuer)}, which is not an https URL as an OpenID issuer identifier is`);
743
+ }
744
+ for (const [index, requirement] of requirements.entries()) {
745
+ if (requirement.claim.trim().length === 0)
746
+ defects.push(`ID token requirement ${index} names no claim`);
747
+ if (requirement.kind === ProviderOAuth2IdTokenRequirementKind.CLAIM_CONTAINS_ANY && requirement.values.length === 0) {
748
+ defects.push(`ID token requirement ${index} (${requirement.claim}) admits no value, so no sign-in could meet it`);
749
+ }
750
+ if (requirement.kind === ProviderOAuth2IdTokenRequirementKind.CLAIM_EQUALS_CLAIM_WHEN_PRESENT && requirement.equalsClaim.trim().length === 0) {
751
+ defects.push(`ID token requirement ${index} (${requirement.claim}) names no claim to equal`);
752
+ }
753
+ }
754
+ return defects;
755
+ }
350
756
  /** How a placeholder's value is compared with a declared one. */
351
757
  export var ProviderOAuth2ValueComparison;
352
758
  (function (ProviderOAuth2ValueComparison) {