@wildo-ai/external-connectors-models 1.1.4 → 1.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/companion-exports.d.ts.map +1 -1
- package/dist/esm/core/commons.external-connectors-models.schemas.d.ts +23 -132
- package/dist/esm/core/commons.external-connectors-models.schemas.d.ts.map +1 -1
- package/dist/esm/core/commons.external-connectors-models.schemas.js +23 -31
- package/dist/esm/core/commons.external-connectors-models.schemas.js.map +1 -1
- package/dist/esm/core/operation-classification.external-connectors-models.schemas.d.ts.map +1 -1
- package/dist/esm/core/operation-classification.external-connectors-models.schemas.js.map +1 -1
- package/dist/esm/core/security.external-connectors-models.schemas.d.ts +40 -8
- package/dist/esm/core/security.external-connectors-models.schemas.d.ts.map +1 -1
- package/dist/esm/core/security.external-connectors-models.schemas.js +61 -14
- package/dist/esm/core/security.external-connectors-models.schemas.js.map +1 -1
- package/dist/esm/index.d.ts +5 -2
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +5 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/protocols/agent-client-protocol.external-connectors-models.schemas.d.ts +78 -163
- package/dist/esm/protocols/agent-client-protocol.external-connectors-models.schemas.d.ts.map +1 -1
- package/dist/esm/protocols/agent-client-protocol.external-connectors-models.schemas.js +78 -93
- package/dist/esm/protocols/agent-client-protocol.external-connectors-models.schemas.js.map +1 -1
- package/dist/esm/protocols/billing.standard-interface.schemas.d.ts +258 -13
- package/dist/esm/protocols/billing.standard-interface.schemas.d.ts.map +1 -1
- package/dist/esm/protocols/billing.standard-interface.schemas.js +41 -0
- package/dist/esm/protocols/billing.standard-interface.schemas.js.map +1 -1
- package/dist/esm/protocols/document-extraction.external-connectors-models.schemas.d.ts +57 -17
- package/dist/esm/protocols/document-extraction.external-connectors-models.schemas.d.ts.map +1 -1
- package/dist/esm/protocols/document-extraction.external-connectors-models.schemas.js +26 -7
- package/dist/esm/protocols/document-extraction.external-connectors-models.schemas.js.map +1 -1
- package/dist/esm/protocols/email-inbound.standard-interface.schemas.d.ts +229 -0
- package/dist/esm/protocols/email-inbound.standard-interface.schemas.d.ts.map +1 -0
- package/dist/esm/protocols/email-inbound.standard-interface.schemas.js +160 -0
- package/dist/esm/protocols/email-inbound.standard-interface.schemas.js.map +1 -0
- package/dist/esm/protocols/email.external-connectors-models.schemas.d.ts +1 -56
- package/dist/esm/protocols/email.external-connectors-models.schemas.d.ts.map +1 -1
- package/dist/esm/protocols/email.external-connectors-models.schemas.js +0 -20
- package/dist/esm/protocols/email.external-connectors-models.schemas.js.map +1 -1
- package/dist/esm/protocols/email.standard-interface.schemas.d.ts +4 -4
- package/dist/esm/protocols/email.standard-interface.schemas.d.ts.map +1 -1
- package/dist/esm/protocols/embeddings.external-connectors-models.schemas.d.ts +1 -16
- package/dist/esm/protocols/embeddings.external-connectors-models.schemas.d.ts.map +1 -1
- package/dist/esm/protocols/embeddings.external-connectors-models.schemas.js +4 -9
- package/dist/esm/protocols/embeddings.external-connectors-models.schemas.js.map +1 -1
- package/dist/esm/protocols/file-storage.standard-interface.d.ts +179 -0
- package/dist/esm/protocols/file-storage.standard-interface.d.ts.map +1 -0
- package/dist/esm/protocols/file-storage.standard-interface.js +79 -0
- package/dist/esm/protocols/file-storage.standard-interface.js.map +1 -0
- package/dist/esm/protocols/frontend.external-connectors-models.schemas.d.ts +6 -13
- package/dist/esm/protocols/frontend.external-connectors-models.schemas.d.ts.map +1 -1
- package/dist/esm/protocols/frontend.external-connectors-models.schemas.js +0 -9
- package/dist/esm/protocols/frontend.external-connectors-models.schemas.js.map +1 -1
- package/dist/esm/protocols/hooks-capability.external-connectors-models.schemas.d.ts.map +1 -1
- package/dist/esm/protocols/hooks-capability.external-connectors-models.schemas.js.map +1 -1
- package/dist/esm/protocols/hooks.external-connectors-models.schemas.d.ts +20 -16
- package/dist/esm/protocols/hooks.external-connectors-models.schemas.d.ts.map +1 -1
- package/dist/esm/protocols/hooks.external-connectors-models.schemas.js +12 -9
- package/dist/esm/protocols/hooks.external-connectors-models.schemas.js.map +1 -1
- package/dist/esm/protocols/image.external-connectors-models.schemas.d.ts +1 -18
- package/dist/esm/protocols/image.external-connectors-models.schemas.d.ts.map +1 -1
- package/dist/esm/protocols/image.external-connectors-models.schemas.js +1 -7
- package/dist/esm/protocols/image.external-connectors-models.schemas.js.map +1 -1
- package/dist/esm/protocols/llm.external-connectors-models.schemas.d.ts +100 -40
- package/dist/esm/protocols/llm.external-connectors-models.schemas.d.ts.map +1 -1
- package/dist/esm/protocols/llm.external-connectors-models.schemas.js +164 -41
- package/dist/esm/protocols/llm.external-connectors-models.schemas.js.map +1 -1
- package/dist/esm/protocols/oauth2-provider-assurance.definitions.d.ts +7 -0
- package/dist/esm/protocols/oauth2-provider-assurance.definitions.d.ts.map +1 -0
- package/dist/esm/protocols/oauth2-provider-assurance.definitions.js +36 -0
- package/dist/esm/protocols/oauth2-provider-assurance.definitions.js.map +1 -0
- package/dist/esm/protocols/oauth2-provider-assurance.schemas.d.ts +23 -0
- package/dist/esm/protocols/oauth2-provider-assurance.schemas.d.ts.map +1 -0
- package/dist/esm/protocols/oauth2-provider-assurance.schemas.js +25 -0
- package/dist/esm/protocols/oauth2-provider-assurance.schemas.js.map +1 -0
- package/dist/esm/protocols/oauth2.external-connectors-models.schemas.d.ts +0 -148
- package/dist/esm/protocols/oauth2.external-connectors-models.schemas.d.ts.map +1 -1
- package/dist/esm/protocols/oauth2.external-connectors-models.schemas.js +1 -40
- package/dist/esm/protocols/oauth2.external-connectors-models.schemas.js.map +1 -1
- package/dist/esm/protocols/provider-oauth2-token-endpoint-hosts.external-connectors-models.d.ts +39 -0
- package/dist/esm/protocols/provider-oauth2-token-endpoint-hosts.external-connectors-models.d.ts.map +1 -0
- package/dist/esm/protocols/provider-oauth2-token-endpoint-hosts.external-connectors-models.js +66 -0
- package/dist/esm/protocols/provider-oauth2-token-endpoint-hosts.external-connectors-models.js.map +1 -0
- package/dist/esm/protocols/rest-operations.external-connectors-models.schemas.d.ts +863 -16
- package/dist/esm/protocols/rest-operations.external-connectors-models.schemas.d.ts.map +1 -1
- package/dist/esm/protocols/rest-operations.external-connectors-models.schemas.js +703 -0
- package/dist/esm/protocols/rest-operations.external-connectors-models.schemas.js.map +1 -1
- package/dist/esm/protocols/sms.external-connectors-models.schemas.d.ts +0 -39
- package/dist/esm/protocols/sms.external-connectors-models.schemas.d.ts.map +1 -1
- package/dist/esm/protocols/sms.external-connectors-models.schemas.js +0 -13
- package/dist/esm/protocols/sms.external-connectors-models.schemas.js.map +1 -1
- package/dist/esm/protocols/sms.standard-interface.schemas.d.ts +1 -1
- package/dist/esm/protocols/sms.standard-interface.schemas.d.ts.map +1 -1
- package/dist/esm/protocols/sms.standard-interface.schemas.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +2 -3
- package/dist/esm/core/protocols.external-connectors-models.schemas.d.ts +0 -74
- package/dist/esm/core/protocols.external-connectors-models.schemas.d.ts.map +0 -1
- package/dist/esm/core/protocols.external-connectors-models.schemas.js +0 -1
- package/dist/esm/core/protocols.external-connectors-models.schemas.js.map +0 -1
- package/dist/esm/protocols/rest-api.external-connectors-models.schemas.d.ts +0 -37
- package/dist/esm/protocols/rest-api.external-connectors-models.schemas.d.ts.map +0 -1
- package/dist/esm/protocols/rest-api.external-connectors-models.schemas.js +0 -10
- package/dist/esm/protocols/rest-api.external-connectors-models.schemas.js.map +0 -1
|
@@ -19,7 +19,31 @@
|
|
|
19
19
|
* path template, where its parameters ride — and none of that is backend runtime. The arrow already
|
|
20
20
|
* pointed this way, since this package has no dependency on the private backend and the private
|
|
21
21
|
* backend depends on it.
|
|
22
|
+
*
|
|
23
|
+
* ## This is the ONLY description of a REST provider — two earlier ones were deleted
|
|
24
|
+
*
|
|
25
|
+
* `rest-api.external-connectors-models.schemas.ts` described REST providers twice over, and both
|
|
26
|
+
* descriptions are gone. Neither should come back beside this module.
|
|
27
|
+
*
|
|
28
|
+
* - **An OpenAPI-shaped operation layer** (`REST_API_OperationSpecSchema`,
|
|
29
|
+
* `REST_API_OperationContextSchema`, `REST_API_OperationDefinition`, `API_Parameter*`,
|
|
30
|
+
* `API_ContentType`) — deleted 2026-09-06 by #331. Ten exported symbols and zero code readers: a
|
|
31
|
+
* provider that authored one compiled, shipped, and was invisible to every runtime.
|
|
32
|
+
* - **A profile / configuration / status triple** (`REST_API_Provider_Profile`,
|
|
33
|
+
* `REST_API_Provider_Configuration`, `REST_APIProvider_StatusSchema`) — deleted 2026-09-12 (#18).
|
|
34
|
+
* Its only reader was the `ExternalProvider_ExchangeProtocol` union, itself read by nothing but a
|
|
35
|
+
* drift test pinning it to the enum, and deleted in the same change. Its `pagination.style` was an
|
|
36
|
+
* anonymous `"offset" | "cursor" | "page" | "link"` restating {@link ProviderRestPaginationStyle}.
|
|
37
|
+
*
|
|
38
|
+
* WHERE a REST API lives and how it authenticates is the `rest` arm of the backend runtime contract
|
|
39
|
+
* (`@wildo-ai/external-connectors-private/backend`); WHAT can be called against it is
|
|
40
|
+
* {@link ProviderRestOperation}, executed by `ProviderOperationExecutorBackendService`. A second
|
|
41
|
+
* description of either is not a missing feature — it is the drift that makes an authoring surface
|
|
42
|
+
* untrustworthy, because an author cannot tell which of two shapes is the one that runs. If an
|
|
43
|
+
* OpenAPI document is ever read (a second mapper input), it feeds `ProviderRestOperation` like
|
|
44
|
+
* every other source.
|
|
22
45
|
*/
|
|
46
|
+
/** @wildo_source:part:start external-connectors.models.rest-operation-contract facet:layer:shared facet:family:provider facet:audience:app-developer */
|
|
23
47
|
/** Where a declared parameter rides on the wire. */
|
|
24
48
|
export var ProviderRestParameterLocation;
|
|
25
49
|
(function (ProviderRestParameterLocation) {
|
|
@@ -40,7 +64,31 @@ export var ProviderRestRequestBodyEncoding;
|
|
|
40
64
|
ProviderRestRequestBodyEncoding["MULTIPART"] = "multipart";
|
|
41
65
|
/** A raw text body; `contentType` on the declaration says which media type is sent. */
|
|
42
66
|
ProviderRestRequestBodyEncoding["TEXT"] = "text";
|
|
67
|
+
/**
|
|
68
|
+
* Raw bytes (#367, folding #364) — an image, a PDF, a file upload a vendor takes as the whole body.
|
|
69
|
+
* `contentType` is REQUIRED, by the TEXT arm's rule: a body without its media type is not addressable.
|
|
70
|
+
* The input must supply a `Uint8Array` (a Node `Buffer` is one); nothing is converted into bytes here,
|
|
71
|
+
* because a string encoded under a guessed charset is exactly how an upload arrives corrupted.
|
|
72
|
+
*/
|
|
73
|
+
ProviderRestRequestBodyEncoding["BINARY"] = "binary";
|
|
43
74
|
})(ProviderRestRequestBodyEncoding || (ProviderRestRequestBodyEncoding = {}));
|
|
75
|
+
/**
|
|
76
|
+
* How the vendor's ANSWER is read (#367, folding #364).
|
|
77
|
+
*
|
|
78
|
+
* The request side could already send TEXT and the transport could already read TEXT and NONE, while
|
|
79
|
+
* the operation declaration could only say JSON — so an operation against a `text/csv` export could be
|
|
80
|
+
* authored and then failed at runtime as `INVALID_JSON`, a contract-breach message for a declaration
|
|
81
|
+
* the contract had no way to state. This is the missing half of that asymmetry.
|
|
82
|
+
*/
|
|
83
|
+
export var ProviderRestResponseDecoding;
|
|
84
|
+
(function (ProviderRestResponseDecoding) {
|
|
85
|
+
/** Parse the body as JSON. The default, and what an absent `responseDecoding` means. */
|
|
86
|
+
ProviderRestResponseDecoding["JSON"] = "json";
|
|
87
|
+
/** Keep the body as text — a CSV export, an XML document. The operation's output is that string. */
|
|
88
|
+
ProviderRestResponseDecoding["TEXT"] = "text";
|
|
89
|
+
/** Read nothing — a write whose answer carries no information (a 204). The output is `undefined`. */
|
|
90
|
+
ProviderRestResponseDecoding["NONE"] = "none";
|
|
91
|
+
})(ProviderRestResponseDecoding || (ProviderRestResponseDecoding = {}));
|
|
44
92
|
/**
|
|
45
93
|
* How a listing operation continues. A HINT the caller reads, never something the executor acts
|
|
46
94
|
* on: an executor that paged on its own would decide a caller's memory budget for it, and the
|
|
@@ -54,6 +102,661 @@ export var ProviderRestPaginationStyle;
|
|
|
54
102
|
/** The vendor returns its own continuation URL (`@odata.nextLink`, `links.next`). */
|
|
55
103
|
ProviderRestPaginationStyle["LINK"] = "link";
|
|
56
104
|
})(ProviderRestPaginationStyle || (ProviderRestPaginationStyle = {}));
|
|
105
|
+
/** @wildo_source:part:start external-connectors.models.connection-rate-budgets facet:layer:shared facet:family:provider facet:audience:app-developer */
|
|
106
|
+
/**
|
|
107
|
+
* WHOSE allowance a vendor counts a request against, under one connection (#1729).
|
|
108
|
+
*
|
|
109
|
+
* A connection is one application registration at the vendor (one OAuth client, one API project) — the
|
|
110
|
+
* base provider's, however many refs spend it. Vendors meter on two axes of it, and a provider declares the
|
|
111
|
+
* one each of its limits uses:
|
|
112
|
+
*
|
|
113
|
+
* | vendor wording | member |
|
|
114
|
+
* |---|---|
|
|
115
|
+
* | Microsoft Graph "per app across all tenants"; Google "per minute per project" | {@link CONNECTION} |
|
|
116
|
+
* | Google "per minute per user per project"; Dataverse "for each user"; Graph "per app per tenant" / "per app and mailbox" when the grant owner IS that tenant or mailbox | {@link OWNER} |
|
|
117
|
+
*
|
|
118
|
+
* spec-cite: https://learn.microsoft.com/en-us/graph/throttling-limits — the global-limits table ("Per app across all
|
|
119
|
+
* tenants") and the scope list "per app across all tenants, per tenant for all apps, per app per tenant"; and
|
|
120
|
+
* https://developers.google.com/workspace/drive/api/guides/limits — "per minute per project" / "per minute per user per
|
|
121
|
+
* project". Fetched 2026-09-28.
|
|
122
|
+
*
|
|
123
|
+
* Deliberately NOT a member: "per tenant for all apps" (Graph). It counts OTHER applications' calls, which this
|
|
124
|
+
* deployment cannot see, so no local counter can hold it; the vendor's `429` is the only honest signal there.
|
|
125
|
+
*/
|
|
126
|
+
export var ProviderRateBudgetPartition;
|
|
127
|
+
(function (ProviderRateBudgetPartition) {
|
|
128
|
+
/** One allowance for the whole connection — every ref, every account it acts for, every process. */
|
|
129
|
+
ProviderRateBudgetPartition["CONNECTION"] = "connection";
|
|
130
|
+
/** One allowance per account the connection acts for — the grant's owner (a person, an organization, the deployment). */
|
|
131
|
+
ProviderRateBudgetPartition["OWNER"] = "owner";
|
|
132
|
+
})(ProviderRateBudgetPartition || (ProviderRateBudgetPartition = {}));
|
|
133
|
+
/**
|
|
134
|
+
* Where a budget was COUNTED, as the call that spent it observed (#1729): `shared` when the deployment's shared store
|
|
135
|
+
* (Redis) counted it for every process of the application — backend replicas, minions, workers — and `process` when
|
|
136
|
+
* only this process did, because no shared store is configured here or it did not answer. Recorded on the call's audit
|
|
137
|
+
* row, so a per-process count is visible rather than implied.
|
|
138
|
+
*/
|
|
139
|
+
export var RateBudgetCounting;
|
|
140
|
+
(function (RateBudgetCounting) {
|
|
141
|
+
RateBudgetCounting["SHARED"] = "shared";
|
|
142
|
+
RateBudgetCounting["PROCESS"] = "process";
|
|
143
|
+
})(RateBudgetCounting || (RateBudgetCounting = {}));
|
|
144
|
+
/**
|
|
145
|
+
* The defects of a connection's rate-budget declarations: a non-positive number, and a contract-level list beside
|
|
146
|
+
* `oauth2.audiences` — which would apply to nothing, since every call of such a connection is on one audience. A pure
|
|
147
|
+
* function so the registry refuses it at boot and a test pins it without a host.
|
|
148
|
+
*/
|
|
149
|
+
export function describeProviderRateBudgetDefects(input) {
|
|
150
|
+
const defects = [];
|
|
151
|
+
const check = (where, declarations) => {
|
|
152
|
+
for (const [index, declaration] of (declarations ?? []).entries()) {
|
|
153
|
+
if (!Number.isInteger(declaration.maxRequests) || declaration.maxRequests < 1) {
|
|
154
|
+
defects.push(`${where}[${index}].maxRequests must be a positive integer (got ${String(declaration.maxRequests)})`);
|
|
155
|
+
}
|
|
156
|
+
if (!Number.isFinite(declaration.windowSeconds) || declaration.windowSeconds <= 0) {
|
|
157
|
+
defects.push(`${where}[${index}].windowSeconds must be positive (got ${String(declaration.windowSeconds)})`);
|
|
158
|
+
}
|
|
159
|
+
if (!Object.values(ProviderRateBudgetPartition).includes(declaration.partition)) {
|
|
160
|
+
defects.push(`${where}[${index}].partition '${String(declaration.partition)}' is not a ProviderRateBudgetPartition`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
check('rest.rateBudgets', input.contractRateBudgets);
|
|
165
|
+
for (const [id, audience] of Object.entries(input.audiences ?? {}))
|
|
166
|
+
check(`oauth2.audiences.${id}.rateBudgets`, audience.rateBudgets);
|
|
167
|
+
if ((input.contractRateBudgets?.length ?? 0) > 0 && Object.keys(input.audiences ?? {}).length > 0) {
|
|
168
|
+
defects.push('rest.rateBudgets is declared beside oauth2.audiences and would apply to no call — declare each limit on the audience it meters');
|
|
169
|
+
}
|
|
170
|
+
return defects;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* The declarations that apply to one call: the audience's own when the call's token is for an audience, else the
|
|
174
|
+
* contract's. Nothing is inherited between the two — see {@link describeProviderRateBudgetDefects}.
|
|
175
|
+
*/
|
|
176
|
+
export function selectProviderRateBudgetDeclarations(input) {
|
|
177
|
+
return input.audience !== undefined ? input.audience.rateBudgets ?? [] : input.contractRateBudgets ?? [];
|
|
178
|
+
}
|
|
179
|
+
/** @wildo_source:part:start external-connectors.models.oauth2-audiences facet:layer:shared facet:family:provider facet:audience:app-developer */
|
|
180
|
+
/** The placeholder a {@link ProviderRestOAuth2Audience.mintScopes} entry uses for the https origin of the request's host. */
|
|
181
|
+
export const PROVIDER_OAUTH2_AUDIENCE_ORIGIN_PLACEHOLDER = '{audienceOrigin}';
|
|
182
|
+
/** Whether `host` is admitted by one audience's hosts: an exact value, or `<labels><suffix>` for a declared suffix. */
|
|
183
|
+
export function providerOAuth2AudienceAdmitsHost(audience, host) {
|
|
184
|
+
const candidate = host.toLowerCase();
|
|
185
|
+
if (audience.hosts.values.some((value) => value.toLowerCase() === candidate))
|
|
186
|
+
return true;
|
|
187
|
+
return (audience.hosts.hostSuffixes ?? []).some((suffix) => {
|
|
188
|
+
const lowered = suffix.toLowerCase();
|
|
189
|
+
if (!candidate.endsWith(lowered) || candidate.length === lowered.length)
|
|
190
|
+
return false;
|
|
191
|
+
return PROVIDER_OAUTH2_DNS_NAME_PATTERN.test(candidate.slice(0, candidate.length - lowered.length));
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* The ONE audience whose hosts admit `host`, or `undefined` when none does. The declaration guarantees at most one
|
|
196
|
+
* ({@link describeProviderOAuth2AudienceDefects} refuses overlapping hosts), so the answer never depends on order.
|
|
197
|
+
*/
|
|
198
|
+
export function selectProviderOAuth2AudienceForHost(oauth2, host) {
|
|
199
|
+
for (const [id, audience] of Object.entries(oauth2.audiences ?? {})) {
|
|
200
|
+
if (providerOAuth2AudienceAdmitsHost(audience, host))
|
|
201
|
+
return { id, audience, primary: id === oauth2.primaryAudience };
|
|
202
|
+
}
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* The scopes that mint `audience`'s token for a request to `origin` — `{audienceOrigin}` replaced by that origin.
|
|
207
|
+
* The caller has already admitted the origin's host through {@link selectProviderOAuth2AudienceForHost}; nothing a
|
|
208
|
+
* caller supplies reaches a scope any other way.
|
|
209
|
+
*/
|
|
210
|
+
export function resolveProviderOAuth2AudienceMintScopes(audience, origin) {
|
|
211
|
+
return audience.mintScopes.map((scope) => (scope.startsWith(PROVIDER_OAUTH2_AUDIENCE_ORIGIN_PLACEHOLDER)
|
|
212
|
+
? `${origin}${scope.slice(PROVIDER_OAUTH2_AUDIENCE_ORIGIN_PLACEHOLDER.length)}`
|
|
213
|
+
: scope));
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* The declared audience a scope a connection might REQUEST names (#1671): its id, or `undefined` when the scope names a
|
|
217
|
+
* resource none of the connection's audiences covers. Only meaningful when `audiences` is declared.
|
|
218
|
+
*
|
|
219
|
+
* A scope that is an absolute URI names the resource a token is minted for (`https://graph.microsoft.com/Mail.Send`,
|
|
220
|
+
* `https://contoso.crm.dynamics.com/user_impersonation`): the resource of one of an audience's fixed mint scopes (the
|
|
221
|
+
* mint scope without its last path segment), or — for a mint scope beginning `{audienceOrigin}` — an https origin whose
|
|
222
|
+
* host the audience admits. A scope that is not an absolute URI (`offline_access`, `Files.Read`) names no resource, and
|
|
223
|
+
* the vendor reads it against its default one, the PRIMARY audience: "omitting the resource identifier in the `scope`
|
|
224
|
+
* parameter defaults to Microsoft Graph".
|
|
225
|
+
* spec-cite: https://learn.microsoft.com/en-us/entra/identity-platform/consent-types-developer — the note under
|
|
226
|
+
* "Admin Consent for Delegated Permissions", fetched 2026-09-27.
|
|
227
|
+
*/
|
|
228
|
+
export function findProviderOAuth2ScopeAudience(oauth2, scope) {
|
|
229
|
+
const audiences = oauth2.audiences ?? {};
|
|
230
|
+
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(scope))
|
|
231
|
+
return oauth2.primaryAudience;
|
|
232
|
+
// The https origin's host, read by the same shape the audiences declare (a DNS name, no port, no credentials) rather
|
|
233
|
+
// than by a URL parser: this package runs in a browser-neutral program with no URL global, and a scope that is not
|
|
234
|
+
// that shape names no host an audience could admit.
|
|
235
|
+
const origin = /^https:\/\/([^/?#:@]+)(?:\/|$)/i.exec(scope);
|
|
236
|
+
const host = origin?.[1]?.toLowerCase();
|
|
237
|
+
for (const [id, audience] of Object.entries(audiences)) {
|
|
238
|
+
for (const mintScope of audience.mintScopes) {
|
|
239
|
+
if (mintScope.startsWith(PROVIDER_OAUTH2_AUDIENCE_ORIGIN_PLACEHOLDER)) {
|
|
240
|
+
if (host !== undefined && PROVIDER_OAUTH2_DNS_NAME_PATTERN.test(host) && providerOAuth2AudienceAdmitsHost(audience, host))
|
|
241
|
+
return id;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const resource = mintScope.slice(0, mintScope.lastIndexOf('/'));
|
|
245
|
+
if (resource.length > 0 && scope.startsWith(`${resource}/`))
|
|
246
|
+
return id;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Whether a scope a connection might REQUEST names a resource among the connection's declared audiences (#1671) — see
|
|
253
|
+
* {@link findProviderOAuth2ScopeAudience}. Asking a person to consent to it is asking them to let this application reach
|
|
254
|
+
* THAT resource, so a scope from a member ref or an opted-in augmentation (#1672) is requestable only when it names one
|
|
255
|
+
* the connection declares. `true` without looking further when the connection declares no audiences: it mints one token
|
|
256
|
+
* for one resource and its scopes are the vendor's own vocabulary (Google's scope URIs name permissions, not resources).
|
|
257
|
+
*/
|
|
258
|
+
export function providerOAuth2ScopeNamesDeclaredAudience(oauth2, scope) {
|
|
259
|
+
if (oauth2.audiences === undefined)
|
|
260
|
+
return true;
|
|
261
|
+
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(scope))
|
|
262
|
+
return true;
|
|
263
|
+
return findProviderOAuth2ScopeAudience(oauth2, scope) !== undefined;
|
|
264
|
+
}
|
|
265
|
+
/** @wildo_source:part:end external-connectors.models.oauth2-audiences */
|
|
266
|
+
/** @wildo_source:part:start external-connectors.models.oauth2-incremental-consent facet:layer:shared facet:family:provider facet:audience:app-developer */
|
|
267
|
+
/**
|
|
268
|
+
* How a vendor extends an existing delegated grant with more scopes — see {@link ProviderRestOAuth2.incrementalConsent}.
|
|
269
|
+
*/
|
|
270
|
+
export var ProviderOAuth2IncrementalConsentForm;
|
|
271
|
+
(function (ProviderOAuth2IncrementalConsentForm) {
|
|
272
|
+
/**
|
|
273
|
+
* The vendor keeps what the person already consented to, and a new authorization request naming only the added
|
|
274
|
+
* scopes ADDS them: Microsoft's delegated permissions "will be granted in addition to any other permissions that
|
|
275
|
+
* were already granted for that app (i.e. incremental)".
|
|
276
|
+
* spec-cite: https://learn.microsoft.com/en-us/entra/identity-platform/consent-types-developer — "Incremental and
|
|
277
|
+
* dynamic user consent", fetched 2026-09-27.
|
|
278
|
+
*/
|
|
279
|
+
ProviderOAuth2IncrementalConsentForm["ADDITIVE"] = "ADDITIVE";
|
|
280
|
+
/**
|
|
281
|
+
* The vendor adds the new scopes to the existing grant only when the request carries `include_granted_scopes=true`:
|
|
282
|
+
* "If you set this parameter's value to `true` and the authorization request is granted, then the new access token
|
|
283
|
+
* will also cover any scopes to which the user previously granted the application access."
|
|
284
|
+
* spec-cite: https://developers.google.com/identity/protocols/oauth2/web-server#incrementalAuth — the
|
|
285
|
+
* `include_granted_scopes` parameter row, fetched 2026-09-27.
|
|
286
|
+
*/
|
|
287
|
+
ProviderOAuth2IncrementalConsentForm["INCLUDE_GRANTED_SCOPES"] = "INCLUDE_GRANTED_SCOPES";
|
|
288
|
+
})(ProviderOAuth2IncrementalConsentForm || (ProviderOAuth2IncrementalConsentForm = {}));
|
|
289
|
+
/** @wildo_source:part:end external-connectors.models.oauth2-incremental-consent */
|
|
290
|
+
/**
|
|
291
|
+
* Why a declared audience set cannot be honoured, or an empty list. Checked where a provider is composed, so a
|
|
292
|
+
* malformed declaration never reaches a request:
|
|
293
|
+
* - `primaryAudience` names no declared audience, or is missing while audiences are declared;
|
|
294
|
+
* - the primary audience's mint scopes are not among the authorization's own `scopes` (its token is the one the
|
|
295
|
+
* code redemption returns, so the authorization must be FOR that resource) or use the origin placeholder;
|
|
296
|
+
* - an audience with no host or no mint scope, a suffix without a leading dot, an origin placeholder anywhere but
|
|
297
|
+
* the start of a scope, or any other `{placeholder}` in a mint scope;
|
|
298
|
+
* - two audiences admitting one host — a request host must select exactly one token.
|
|
299
|
+
*/
|
|
300
|
+
export function describeProviderOAuth2AudienceDefects(oauth2) {
|
|
301
|
+
const audiences = oauth2.audiences;
|
|
302
|
+
if (audiences === undefined) {
|
|
303
|
+
return oauth2.primaryAudience === undefined ? [] : [`primaryAudience '${oauth2.primaryAudience}' is declared with no audiences`];
|
|
304
|
+
}
|
|
305
|
+
const defects = [];
|
|
306
|
+
const primary = oauth2.primaryAudience === undefined ? undefined : audiences[oauth2.primaryAudience];
|
|
307
|
+
if (primary === undefined)
|
|
308
|
+
defects.push(`primaryAudience '${String(oauth2.primaryAudience)}' names no declared audience`);
|
|
309
|
+
for (const [id, audience] of Object.entries(audiences)) {
|
|
310
|
+
if (audience.hosts.values.length === 0 && (audience.hosts.hostSuffixes ?? []).length === 0)
|
|
311
|
+
defects.push(`audience '${id}' admits no host`);
|
|
312
|
+
for (const suffix of audience.hosts.hostSuffixes ?? []) {
|
|
313
|
+
if (!suffix.startsWith('.') || !PROVIDER_OAUTH2_DNS_NAME_PATTERN.test(suffix.slice(1).toLowerCase()))
|
|
314
|
+
defects.push(`audience '${id}' declares the host suffix '${suffix}', which is not '.<dns-name>'`);
|
|
315
|
+
}
|
|
316
|
+
for (const value of audience.hosts.values) {
|
|
317
|
+
if (!PROVIDER_OAUTH2_DNS_NAME_PATTERN.test(value.toLowerCase()))
|
|
318
|
+
defects.push(`audience '${id}' declares the host '${value}', which is not a DNS name`);
|
|
319
|
+
}
|
|
320
|
+
if (audience.mintScopes.length === 0)
|
|
321
|
+
defects.push(`audience '${id}' declares no mint scope`);
|
|
322
|
+
for (const scope of audience.mintScopes) {
|
|
323
|
+
const rest = scope.startsWith(PROVIDER_OAUTH2_AUDIENCE_ORIGIN_PLACEHOLDER) ? scope.slice(PROVIDER_OAUTH2_AUDIENCE_ORIGIN_PLACEHOLDER.length) : scope;
|
|
324
|
+
if (rest.includes('{') || rest.includes('}'))
|
|
325
|
+
defects.push(`audience '${id}' mint scope '${scope}' carries a placeholder other than a leading ${PROVIDER_OAUTH2_AUDIENCE_ORIGIN_PLACEHOLDER}`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (primary !== undefined) {
|
|
329
|
+
for (const scope of primary.mintScopes) {
|
|
330
|
+
if (scope.startsWith(PROVIDER_OAUTH2_AUDIENCE_ORIGIN_PLACEHOLDER))
|
|
331
|
+
defects.push(`the primary audience '${oauth2.primaryAudience}' mint scope '${scope}' names a per-request origin; the authorization must be for one fixed resource`);
|
|
332
|
+
else if (!(oauth2.scopes ?? []).includes(scope))
|
|
333
|
+
defects.push(`the primary audience '${oauth2.primaryAudience}' mint scope '${scope}' is not among the authorization's own scopes`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
const entries = Object.entries(audiences);
|
|
337
|
+
for (let left = 0; left < entries.length; left += 1) {
|
|
338
|
+
for (let right = left + 1; right < entries.length; right += 1) {
|
|
339
|
+
const [leftId, leftAudience] = entries[left];
|
|
340
|
+
const [rightId, rightAudience] = entries[right];
|
|
341
|
+
const overlap = leftAudience.hosts.values.some((host) => providerOAuth2AudienceAdmitsHost(rightAudience, host))
|
|
342
|
+
|| rightAudience.hosts.values.some((host) => providerOAuth2AudienceAdmitsHost(leftAudience, host))
|
|
343
|
+
|| (leftAudience.hosts.hostSuffixes ?? []).some((suffix) => (rightAudience.hosts.hostSuffixes ?? []).some((other) => suffix.toLowerCase().endsWith(other.toLowerCase()) || other.toLowerCase().endsWith(suffix.toLowerCase())));
|
|
344
|
+
if (overlap)
|
|
345
|
+
defects.push(`audiences '${leftId}' and '${rightId}' admit a common host; a request host must select one token`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return defects;
|
|
349
|
+
}
|
|
350
|
+
/** How a placeholder's value is compared with a declared one. */
|
|
351
|
+
export var ProviderOAuth2ValueComparison;
|
|
352
|
+
(function (ProviderOAuth2ValueComparison) {
|
|
353
|
+
/** A host or an origin: DNS names compare case-insensitively. spec-cite: RFC 4343 §2. */
|
|
354
|
+
ProviderOAuth2ValueComparison["CASE_INSENSITIVE"] = "CASE_INSENSITIVE";
|
|
355
|
+
/** A scope value: scope strings are case-sensitive. spec-cite: RFC 6749 §3.3. */
|
|
356
|
+
ProviderOAuth2ValueComparison["EXACT"] = "EXACT";
|
|
357
|
+
})(ProviderOAuth2ValueComparison || (ProviderOAuth2ValueComparison = {}));
|
|
358
|
+
/**
|
|
359
|
+
* The declared reason a value is unsupported, or `undefined` when it is not declared unsupported. Compared as the
|
|
360
|
+
* placeholder's values are: case-insensitively for a host or origin, exactly for a scope value.
|
|
361
|
+
*/
|
|
362
|
+
export function findProviderOAuth2UnsupportedValueReason(unsupportedValues, value, comparison) {
|
|
363
|
+
if (unsupportedValues === undefined)
|
|
364
|
+
return undefined;
|
|
365
|
+
const key = comparison === ProviderOAuth2ValueComparison.EXACT
|
|
366
|
+
? (Object.hasOwn(unsupportedValues, value) ? value : undefined)
|
|
367
|
+
: Object.keys(unsupportedValues).find((candidate) => candidate.toLowerCase() === value.toLowerCase());
|
|
368
|
+
return key === undefined ? undefined : unsupportedValues[key];
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Why a declared unsupported-value map contradicts the admitted set it sits beside, or an empty list: a value both
|
|
372
|
+
* admitted and declined would make the refusal reason a lie, and a declined value with no reason says nothing.
|
|
373
|
+
*/
|
|
374
|
+
function describeProviderOAuth2UnsupportedValueDefects(unsupportedValues, admitted, comparison) {
|
|
375
|
+
if (unsupportedValues === undefined)
|
|
376
|
+
return [];
|
|
377
|
+
const defects = [];
|
|
378
|
+
const normalise = (value) => (comparison === ProviderOAuth2ValueComparison.EXACT ? value : value.toLowerCase());
|
|
379
|
+
const admittedSet = new Set(admitted.map(normalise));
|
|
380
|
+
for (const [value, reason] of Object.entries(unsupportedValues)) {
|
|
381
|
+
if (admittedSet.has(normalise(value)))
|
|
382
|
+
defects.push(`'${value}' is both admitted and declared unsupported`);
|
|
383
|
+
if (typeof reason !== 'string' || reason.trim() === '')
|
|
384
|
+
defects.push(`unsupported value '${value}' states no reason`);
|
|
385
|
+
}
|
|
386
|
+
return defects;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Where a `{placeholder}` sits inside one OAuth2 SCOPE entry. See {@link classifyProviderOAuth2ScopePlaceholder}.
|
|
390
|
+
*
|
|
391
|
+
* Deliberately not {@link ProviderOAuth2EndpointPlaceholderPosition}: a scope is not a URL, and the
|
|
392
|
+
* endpoint scan would call `{permissions}` an ORIGIN (it begins the string and nothing follows it).
|
|
393
|
+
*/
|
|
394
|
+
export var ProviderOAuth2ScopePlaceholderPosition;
|
|
395
|
+
(function (ProviderOAuth2ScopePlaceholderPosition) {
|
|
396
|
+
/**
|
|
397
|
+
* The placeholder begins the scope and is followed by `/` — `{hostUrl}/.default`. Its value is an
|
|
398
|
+
* https ORIGIN, admitted only within {@link ProviderRestOAuth2.endpointHostAllowances}.
|
|
399
|
+
*/
|
|
400
|
+
ProviderOAuth2ScopePlaceholderPosition["ORIGIN"] = "ORIGIN";
|
|
401
|
+
/**
|
|
402
|
+
* The placeholder is the entire scope entry — `{permissions}`. Its value is one of
|
|
403
|
+
* {@link ProviderRestOAuth2.scopeValueAllowances}.
|
|
404
|
+
*/
|
|
405
|
+
ProviderOAuth2ScopePlaceholderPosition["WHOLE_SCOPE"] = "WHOLE_SCOPE";
|
|
406
|
+
})(ProviderOAuth2ScopePlaceholderPosition || (ProviderOAuth2ScopePlaceholderPosition = {}));
|
|
407
|
+
/** Why a scope entry cannot be resolved safely, whatever is supplied. See {@link findProviderOAuth2ScopeTemplateDefects}. */
|
|
408
|
+
export var ProviderOAuth2ScopeTemplateDefect;
|
|
409
|
+
(function (ProviderOAuth2ScopeTemplateDefect) {
|
|
410
|
+
/**
|
|
411
|
+
* A placeholder sits anywhere except at the start of the scope followed by `/` or the end —
|
|
412
|
+
* `api://{x}/.default`, `{x}.default`, `openid {x}`. Such a value would be concatenated with text
|
|
413
|
+
* around it into a resource or scope nobody declared.
|
|
414
|
+
*/
|
|
415
|
+
ProviderOAuth2ScopeTemplateDefect["UNSUPPORTED_PLACEHOLDER_POSITION"] = "UNSUPPORTED_PLACEHOLDER_POSITION";
|
|
416
|
+
/** More than one placeholder in one scope entry: each value validated alone can compose an undeclared one. */
|
|
417
|
+
ProviderOAuth2ScopeTemplateDefect["SEVERAL_PLACEHOLDERS"] = "SEVERAL_PLACEHOLDERS";
|
|
418
|
+
/** A `{` or `}` that is not part of a well-formed placeholder: a vendor can never grant a scope carrying one. */
|
|
419
|
+
ProviderOAuth2ScopeTemplateDefect["STRAY_BRACE"] = "STRAY_BRACE";
|
|
420
|
+
})(ProviderOAuth2ScopeTemplateDefect || (ProviderOAuth2ScopeTemplateDefect = {}));
|
|
421
|
+
/**
|
|
422
|
+
* The defects that make a scope entry unsafe to resolve, or an empty list. Shared by the runtime (which
|
|
423
|
+
* refuses) and the transposer (which refuses to emit), so neither admits a shape the other rejects.
|
|
424
|
+
*/
|
|
425
|
+
export function findProviderOAuth2ScopeTemplateDefects(scope) {
|
|
426
|
+
const occurrences = [...scope.matchAll(PROVIDER_OAUTH2_ENDPOINT_PLACEHOLDER_PATTERN)];
|
|
427
|
+
const withoutPlaceholders = scope.replace(PROVIDER_OAUTH2_ENDPOINT_PLACEHOLDER_PATTERN, '');
|
|
428
|
+
const defects = [];
|
|
429
|
+
if (withoutPlaceholders.includes('{') || withoutPlaceholders.includes('}'))
|
|
430
|
+
defects.push(ProviderOAuth2ScopeTemplateDefect.STRAY_BRACE);
|
|
431
|
+
if (occurrences.length > 1)
|
|
432
|
+
defects.push(ProviderOAuth2ScopeTemplateDefect.SEVERAL_PLACEHOLDERS);
|
|
433
|
+
const first = occurrences[0];
|
|
434
|
+
if (first !== undefined) {
|
|
435
|
+
const next = scope.charAt((first.index ?? 0) + first[0].length);
|
|
436
|
+
if ((first.index ?? 0) !== 0 || (next !== '' && next !== '/'))
|
|
437
|
+
defects.push(ProviderOAuth2ScopeTemplateDefect.UNSUPPORTED_PLACEHOLDER_POSITION);
|
|
438
|
+
}
|
|
439
|
+
return defects;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Classify the one placeholder a scope entry may carry, or `undefined` when it carries none.
|
|
443
|
+
*
|
|
444
|
+
* ONE function, shared by the transposer and the runtime, like its endpoint counterpart. Meaningful only
|
|
445
|
+
* for a scope with no {@link findProviderOAuth2ScopeTemplateDefects}; every consumer refuses those first.
|
|
446
|
+
*/
|
|
447
|
+
export function classifyProviderOAuth2ScopePlaceholder(scope) {
|
|
448
|
+
const match = /^\{([A-Za-z_][A-Za-z0-9_]*)\}/.exec(scope);
|
|
449
|
+
if (match === null)
|
|
450
|
+
return undefined;
|
|
451
|
+
return {
|
|
452
|
+
name: match[1],
|
|
453
|
+
position: match[0].length === scope.length ? ProviderOAuth2ScopePlaceholderPosition.WHOLE_SCOPE : ProviderOAuth2ScopePlaceholderPosition.ORIGIN,
|
|
454
|
+
token: match[0],
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* One scope token: printable ASCII except space, `"` and backslash.
|
|
459
|
+
* spec-cite: RFC 6749 §3.3 (scope-token = 1*( %x21 / %x23-5B / %x5D-7E )).
|
|
460
|
+
* Braces are additionally refused: a value carrying one would read as a placeholder nobody resolves.
|
|
461
|
+
*/
|
|
462
|
+
const PROVIDER_OAUTH2_SCOPE_TOKEN_PATTERN = /^[\x21\x23-\x5B\x5D-\x7A\x7C\x7E]+$/;
|
|
463
|
+
/**
|
|
464
|
+
* Why a declared whole-scope allowance cannot be applied, or an empty list. Enforced at generation so a
|
|
465
|
+
* broken set refuses the batch; the runtime matches exactly and never trusts a value it did not declare.
|
|
466
|
+
*/
|
|
467
|
+
export function describeProviderOAuth2ScopeValueAllowanceDefects(allowance) {
|
|
468
|
+
const defects = [];
|
|
469
|
+
if (!Array.isArray(allowance.values) || allowance.values.length === 0)
|
|
470
|
+
return ['a scope value allowance admits nothing'];
|
|
471
|
+
for (const value of allowance.values) {
|
|
472
|
+
const tokens = typeof value === 'string' ? value.split(' ') : [];
|
|
473
|
+
if (typeof value !== 'string' || value.length === 0 || !tokens.every((token) => PROVIDER_OAUTH2_SCOPE_TOKEN_PATTERN.test(token))) {
|
|
474
|
+
defects.push(`'${String(value)}' is not a space-delimited list of scope tokens`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
defects.push(...describeProviderOAuth2UnsupportedValueDefects(allowance.unsupportedValues, allowance.values, ProviderOAuth2ValueComparison.EXACT));
|
|
478
|
+
return defects;
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Where a `{placeholder}` sits inside an OAuth2 endpoint template — the classification the host
|
|
482
|
+
* allow-list keys on. See {@link classifyProviderOAuth2EndpointPlaceholders}.
|
|
483
|
+
*/
|
|
484
|
+
export var ProviderOAuth2EndpointPlaceholderPosition;
|
|
485
|
+
(function (ProviderOAuth2EndpointPlaceholderPosition) {
|
|
486
|
+
/**
|
|
487
|
+
* The placeholder BEGINS the template, so its value is the whole scheme + authority (+ any path
|
|
488
|
+
* prefix): `{baseUrl}/login/oauth/authorize`. The most dangerous position — the value names the
|
|
489
|
+
* scheme as well as the host.
|
|
490
|
+
*/
|
|
491
|
+
ProviderOAuth2EndpointPlaceholderPosition["ORIGIN"] = "ORIGIN";
|
|
492
|
+
/**
|
|
493
|
+
* The placeholder sits between `://` and the first `/`, `?` or `#` after it:
|
|
494
|
+
* `https://{cloud}/common/…`, `https://accounts.{location}/…`. Its value decides which host
|
|
495
|
+
* receives the request.
|
|
496
|
+
*/
|
|
497
|
+
ProviderOAuth2EndpointPlaceholderPosition["HOST"] = "HOST";
|
|
498
|
+
/** Anywhere after the authority. Percent-encoded, so it cannot move the request to another host. */
|
|
499
|
+
ProviderOAuth2EndpointPlaceholderPosition["PATH_OR_QUERY"] = "PATH_OR_QUERY";
|
|
500
|
+
})(ProviderOAuth2EndpointPlaceholderPosition || (ProviderOAuth2EndpointPlaceholderPosition = {}));
|
|
501
|
+
/** How a host- or origin-forming placeholder may be filled. See {@link ProviderRestOAuth2.endpointHostAllowances}. */
|
|
502
|
+
export var ProviderOAuth2EndpointHostAllowanceKind;
|
|
503
|
+
(function (ProviderOAuth2EndpointHostAllowanceKind) {
|
|
504
|
+
/**
|
|
505
|
+
* Chosen at connect time from values the provider DECLARES — Microsoft's commercial login cloud,
|
|
506
|
+
* Zoho's data centres. A value outside the set is refused; a declared value is substituted
|
|
507
|
+
* verbatim (it is a hostname or origin, not a path segment to encode).
|
|
508
|
+
*
|
|
509
|
+
* `hostSuffixes` additionally admits `<dns-label>(.<dns-label>)*` followed by one of the suffixes,
|
|
510
|
+
* for a vendor whose per-customer host is a subdomain it controls (Salesforce My Domain,
|
|
511
|
+
* `<name>.my.salesforce.com`). On a HOST placeholder a suffix applies only when the placeholder is
|
|
512
|
+
* the WHOLE host. On an ORIGIN placeholder it admits exactly `https://<dns-labels><suffix>` — the
|
|
513
|
+
* scheme fixed to https, no port, path, userinfo, query or fragment — which is what a per-customer
|
|
514
|
+
* origin such as a Dataverse environment URL (`https://<org>.crm4.dynamics.com`, #991) needs.
|
|
515
|
+
*/
|
|
516
|
+
ProviderOAuth2EndpointHostAllowanceKind["DECLARED_VALUES"] = "DECLARED_VALUES";
|
|
517
|
+
/**
|
|
518
|
+
* Filled from the DEPLOYMENT's configured base URL for this provider — the REST contract's
|
|
519
|
+
* `baseUrl`, or the environment variable its `baseUrlEnvVarName` names. For a self-hosted vendor
|
|
520
|
+
* (Gitea) the operator names the instance once; a person connecting an account is never asked, and
|
|
521
|
+
* a value they supply is refused. Admitted in the ORIGIN position only.
|
|
522
|
+
*/
|
|
523
|
+
ProviderOAuth2EndpointHostAllowanceKind["DEPLOYMENT_BASE_URL"] = "DEPLOYMENT_BASE_URL";
|
|
524
|
+
/**
|
|
525
|
+
* Filled from a deployment environment variable dedicated to THIS placeholder, named by the
|
|
526
|
+
* allowance's `envVarName` (#992). For a vendor whose endpoints sit on more than one per-tenant host
|
|
527
|
+
* — Workday's `https://{authHost}/…/authorize` and `https://{apiHost}/…/token` — one base URL cannot
|
|
528
|
+
* name both, so each host is its own operator setting (`WORKDAY_AUTH_HOST`, `WORKDAY_API_HOST`).
|
|
529
|
+
*
|
|
530
|
+
* Like DEPLOYMENT_BASE_URL, never asked of a person connecting, and a value they supply is refused.
|
|
531
|
+
* The trade is explicit: the application serves the one vendor tenant (data centre) the operator
|
|
532
|
+
* names. Admitted on a HOST placeholder that is the whole host, with a DNS-name value.
|
|
533
|
+
*/
|
|
534
|
+
ProviderOAuth2EndpointHostAllowanceKind["DEPLOYMENT_CONFIGURED"] = "DEPLOYMENT_CONFIGURED";
|
|
535
|
+
/**
|
|
536
|
+
* DERIVED from another connect-time answer through a declared table (#1937): Microsoft Graph's operation host follows
|
|
537
|
+
* the login cloud the connection chose (`login.microsoftonline.com` → `graph.microsoft.com`), because a token minted by
|
|
538
|
+
* one national cloud is not accepted by another. The Microsoft source also maps the US Government login cloud; that
|
|
539
|
+
* row is declined (`unsupportedValues`), because US Government clouds are out of scope (decision of 2026-09-29).
|
|
540
|
+
*
|
|
541
|
+
* Never asked of a person and never taken from a request: the value is looked up from the ANSWER the connection
|
|
542
|
+
* stored for `answer`, and an answer the table does not name is refused. `whenUnanswered` is the host for a connection
|
|
543
|
+
* that answered nothing (the application's own pasted credential); absent, such a connection is refused. The table is
|
|
544
|
+
* the allow-list, so no other host can result. Admitted on a HOST placeholder that is the whole host.
|
|
545
|
+
*
|
|
546
|
+
* spec-cite: https://learn.microsoft.com/en-us/graph/deployments — "Access tokens acquired for a national cloud
|
|
547
|
+
* deployment are not interchangeable with those acquired for the global service or any other national cloud."
|
|
548
|
+
*/
|
|
549
|
+
ProviderOAuth2EndpointHostAllowanceKind["DERIVED_FROM_CONNECTION_ANSWER"] = "DERIVED_FROM_CONNECTION_ANSWER";
|
|
550
|
+
})(ProviderOAuth2EndpointHostAllowanceKind || (ProviderOAuth2EndpointHostAllowanceKind = {}));
|
|
551
|
+
/**
|
|
552
|
+
* Why an OAuth2 endpoint TEMPLATE cannot be resolved safely, whatever values are supplied.
|
|
553
|
+
* See {@link findProviderOAuth2EndpointTemplateDefects}.
|
|
554
|
+
*/
|
|
555
|
+
export var ProviderOAuth2EndpointTemplateDefect;
|
|
556
|
+
(function (ProviderOAuth2EndpointTemplateDefect) {
|
|
557
|
+
/**
|
|
558
|
+
* The template neither begins `http://` / `https://` nor begins with a placeholder. The WHATWG URL
|
|
559
|
+
* parser reads `https:/{h}/t`, `https:{h}/t` and `https:` + two backslashes + `{h}/t` as `https://h/t`, so a string scan
|
|
560
|
+
* for `://` would call `{h}` a PATH placeholder, encode it (a hostname survives encoding untouched),
|
|
561
|
+
* and send the request — and the client secret — to whatever a caller typed.
|
|
562
|
+
*/
|
|
563
|
+
ProviderOAuth2EndpointTemplateDefect["NOT_AN_ABSOLUTE_TEMPLATE"] = "NOT_AN_ABSOLUTE_TEMPLATE";
|
|
564
|
+
/**
|
|
565
|
+
* An ORIGIN placeholder is followed by something other than `/`, `?`, `#` or the end —
|
|
566
|
+
* `{baseUrl}.evil.test/…`, `{baseUrl}:8443/…`, `{a}{b}/…`. The text after it would extend the host
|
|
567
|
+
* the origin value names.
|
|
568
|
+
*/
|
|
569
|
+
ProviderOAuth2EndpointTemplateDefect["ORIGIN_NOT_TERMINATED"] = "ORIGIN_NOT_TERMINATED";
|
|
570
|
+
/**
|
|
571
|
+
* One authority carries more than one placeholder (`https://{a}{b}/…`, `https://{sub}.{domain}/…`).
|
|
572
|
+
* Each value validated alone can still compose a host no declaration admits.
|
|
573
|
+
*/
|
|
574
|
+
ProviderOAuth2EndpointTemplateDefect["SEVERAL_PLACEHOLDERS_IN_AUTHORITY"] = "SEVERAL_PLACEHOLDERS_IN_AUTHORITY";
|
|
575
|
+
/**
|
|
576
|
+
* The authority after `://` is empty, begins with `/` or a backslash, or carries a backslash or `@`
|
|
577
|
+
* before the first `/`, `?` or `#`. The parser skips extra slashes (`https:///{h}/t` dials `{h}` as the
|
|
578
|
+
* HOST, while the `://` scan sees an empty authority and calls `{h}` a PATH), reads a backslash as a
|
|
579
|
+
* path separator, and
|
|
580
|
+
* reads text before `@` as userinfo — each makes the scan and the parser disagree about the host.
|
|
581
|
+
*/
|
|
582
|
+
ProviderOAuth2EndpointTemplateDefect["MALFORMED_AUTHORITY"] = "MALFORMED_AUTHORITY";
|
|
583
|
+
})(ProviderOAuth2EndpointTemplateDefect || (ProviderOAuth2EndpointTemplateDefect = {}));
|
|
584
|
+
/**
|
|
585
|
+
* A DNS name: one or more LDH labels, lower-case (callers lower-case first).
|
|
586
|
+
* spec-cite: RFC 1123 §2.1 (a label may begin with a digit) and RFC 1035 §2.3.1 (letters, digits,
|
|
587
|
+
* hyphen; 63 octets per label).
|
|
588
|
+
*/
|
|
589
|
+
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])?)*$/;
|
|
590
|
+
/**
|
|
591
|
+
* Why a DECLARED host allowance cannot be applied where its placeholder sits — the position rules the
|
|
592
|
+
* transposer enforces at GENERATION time, so a broken declaration refuses the batch instead of being
|
|
593
|
+
* emitted and refused at every connect. Empty when the allowance is sound.
|
|
594
|
+
*
|
|
595
|
+
* The runtime resolver refuses each of these too; this exists so they are caught where a human is
|
|
596
|
+
* already looking at a diff rather than where a person is pressing Connect.
|
|
597
|
+
*/
|
|
598
|
+
export function describeProviderOAuth2EndpointHostAllowanceDefects(placeholder, allowance) {
|
|
599
|
+
const defects = [];
|
|
600
|
+
if (placeholder.position === ProviderOAuth2EndpointPlaceholderPosition.PATH_OR_QUERY) {
|
|
601
|
+
return ['a PATH or QUERY placeholder takes no host allowance'];
|
|
602
|
+
}
|
|
603
|
+
if (allowance.kind === ProviderOAuth2EndpointHostAllowanceKind.DEPLOYMENT_BASE_URL) {
|
|
604
|
+
if (placeholder.position !== ProviderOAuth2EndpointPlaceholderPosition.ORIGIN) {
|
|
605
|
+
defects.push('DEPLOYMENT_BASE_URL fills an ORIGIN only, and this placeholder does not begin its endpoint');
|
|
606
|
+
}
|
|
607
|
+
if (allowance.baseUrlPathSuffix !== undefined && !/^\/[^?#]*[^/?#]$/.test(allowance.baseUrlPathSuffix)) {
|
|
608
|
+
defects.push(`baseUrlPathSuffix '${allowance.baseUrlPathSuffix}' must begin with '/' and name a path (no trailing '/', query or fragment)`);
|
|
609
|
+
}
|
|
610
|
+
return defects;
|
|
611
|
+
}
|
|
612
|
+
if (allowance.kind === ProviderOAuth2EndpointHostAllowanceKind.DERIVED_FROM_CONNECTION_ANSWER) {
|
|
613
|
+
if (placeholder.position !== ProviderOAuth2EndpointPlaceholderPosition.HOST || !placeholder.occupiesWholeHost) {
|
|
614
|
+
defects.push('DERIVED_FROM_CONNECTION_ANSWER fills a WHOLE HOST only, and this placeholder is not one');
|
|
615
|
+
}
|
|
616
|
+
if (typeof allowance.answer !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(allowance.answer)) {
|
|
617
|
+
defects.push(`answer '${String(allowance.answer)}' is not a placeholder name`);
|
|
618
|
+
}
|
|
619
|
+
const hosts = Object.values(allowance.hosts ?? {});
|
|
620
|
+
if (hosts.length === 0)
|
|
621
|
+
defects.push('DERIVED_FROM_CONNECTION_ANSWER maps no answer to a host, which admits nothing');
|
|
622
|
+
for (const host of [...hosts, ...(allowance.whenUnanswered === undefined ? [] : [allowance.whenUnanswered])]) {
|
|
623
|
+
if (typeof host !== 'string' || !PROVIDER_OAUTH2_DNS_NAME_PATTERN.test(host.toLowerCase()))
|
|
624
|
+
defects.push(`'${String(host)}' is not a DNS name`);
|
|
625
|
+
}
|
|
626
|
+
defects.push(...describeProviderOAuth2UnsupportedValueDefects(allowance.unsupportedValues, Object.keys(allowance.hosts ?? {}), ProviderOAuth2ValueComparison.CASE_INSENSITIVE));
|
|
627
|
+
return defects;
|
|
628
|
+
}
|
|
629
|
+
if (allowance.kind === ProviderOAuth2EndpointHostAllowanceKind.DEPLOYMENT_CONFIGURED) {
|
|
630
|
+
if (placeholder.position !== ProviderOAuth2EndpointPlaceholderPosition.HOST || !placeholder.occupiesWholeHost) {
|
|
631
|
+
defects.push('DEPLOYMENT_CONFIGURED fills a WHOLE HOST only, and this placeholder is not one');
|
|
632
|
+
}
|
|
633
|
+
if (typeof allowance.envVarName !== 'string' || !/^[A-Z][A-Z0-9_]*$/.test(allowance.envVarName)) {
|
|
634
|
+
defects.push(`envVarName '${String(allowance.envVarName)}' is not an upper-case environment variable name`);
|
|
635
|
+
}
|
|
636
|
+
return defects;
|
|
637
|
+
}
|
|
638
|
+
if (allowance.originMatchesDeploymentBaseUrl === true && placeholder.position !== ProviderOAuth2EndpointPlaceholderPosition.ORIGIN) {
|
|
639
|
+
defects.push('originMatchesDeploymentBaseUrl applies to an ORIGIN only, and this placeholder is not one');
|
|
640
|
+
}
|
|
641
|
+
if (allowance.values.length === 0 && (allowance.hostSuffixes ?? []).length === 0) {
|
|
642
|
+
defects.push('DECLARED_VALUES admits nothing');
|
|
643
|
+
}
|
|
644
|
+
for (const value of allowance.values) {
|
|
645
|
+
if (placeholder.position === ProviderOAuth2EndpointPlaceholderPosition.ORIGIN) {
|
|
646
|
+
// Matched as text, not with `URL`: this package compiles without a DOM or Node lib. A lower-case
|
|
647
|
+
// `https://` + DNS name, with nothing after the host but an optional port, is exactly an origin as
|
|
648
|
+
// `URL#origin` would print it back.
|
|
649
|
+
const match = /^https:\/\/([^:/?#@]+)(?::\d{1,5})?$/.exec(value);
|
|
650
|
+
if (match === null || !PROVIDER_OAUTH2_DNS_NAME_PATTERN.test(match[1])) {
|
|
651
|
+
defects.push(`'${value}' is not an https origin (scheme and lower-case host only)`);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
else if (!PROVIDER_OAUTH2_DNS_NAME_PATTERN.test(value.toLowerCase())) {
|
|
655
|
+
defects.push(`'${value}' is not a DNS name`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
for (const suffix of allowance.hostSuffixes ?? []) {
|
|
659
|
+
// An ORIGIN is a whole authority by construction (the scheme is then fixed to https); a HOST
|
|
660
|
+
// placeholder qualifies only when nothing surrounds it inside the authority.
|
|
661
|
+
// A placeholder that OPENS the host qualifies too, but only for the ONE suffix the template itself writes after
|
|
662
|
+
// it (#1015): any other suffix would be concatenated with that literal into a host nobody declared.
|
|
663
|
+
const opensHostWithThisSuffix = placeholder.literalHostSuffix !== undefined && placeholder.literalHostSuffix.toLowerCase() === suffix.toLowerCase();
|
|
664
|
+
if (placeholder.position === ProviderOAuth2EndpointPlaceholderPosition.HOST && !placeholder.occupiesWholeHost && !opensHostWithThisSuffix) {
|
|
665
|
+
defects.push(`host suffix '${suffix}' on a placeholder that is not the whole host`);
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
const labels = suffix.startsWith('.') ? suffix.slice(1) : '';
|
|
669
|
+
if (!PROVIDER_OAUTH2_DNS_NAME_PATTERN.test(labels.toLowerCase()) || !labels.includes('.')) {
|
|
670
|
+
defects.push(`host suffix '${suffix}' must begin with '.' and name at least two DNS labels (a bare '.com' admits every host under a TLD)`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
defects.push(...describeProviderOAuth2UnsupportedValueDefects(allowance.unsupportedValues, allowance.values, ProviderOAuth2ValueComparison.CASE_INSENSITIVE));
|
|
674
|
+
for (const declined of Object.keys(allowance.unsupportedValues ?? {})) {
|
|
675
|
+
// A declined value a declared SUFFIX would still admit is admitted, whatever the reason says.
|
|
676
|
+
const coveringSuffix = (allowance.hostSuffixes ?? []).find((suffix) => declined.toLowerCase().endsWith(suffix.toLowerCase()));
|
|
677
|
+
if (coveringSuffix !== undefined)
|
|
678
|
+
defects.push(`'${declined}' is declared unsupported but the host suffix '${coveringSuffix}' admits it`);
|
|
679
|
+
}
|
|
680
|
+
return defects;
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* The defects that make a template unsafe to resolve, or an empty list. Shared by the runtime (which
|
|
684
|
+
* refuses) and the transposer (which refuses to emit), so neither can admit a shape the other rejects.
|
|
685
|
+
*/
|
|
686
|
+
export function findProviderOAuth2EndpointTemplateDefects(template) {
|
|
687
|
+
const defects = [];
|
|
688
|
+
const leadingPlaceholder = /^\{[A-Za-z_][A-Za-z0-9_]*\}/.exec(template);
|
|
689
|
+
if (!/^https?:\/\//i.test(template) && leadingPlaceholder === null) {
|
|
690
|
+
return [ProviderOAuth2EndpointTemplateDefect.NOT_AN_ABSOLUTE_TEMPLATE];
|
|
691
|
+
}
|
|
692
|
+
if (leadingPlaceholder !== null) {
|
|
693
|
+
const next = template.charAt(leadingPlaceholder[0].length);
|
|
694
|
+
if (next !== '' && next !== '/' && next !== '?' && next !== '#')
|
|
695
|
+
defects.push(ProviderOAuth2EndpointTemplateDefect.ORIGIN_NOT_TERMINATED);
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
const afterScheme = template.slice(template.indexOf('://') + 3);
|
|
699
|
+
const authorityEnd = afterScheme.search(/[/?#]/);
|
|
700
|
+
const authority = authorityEnd < 0 ? afterScheme : afterScheme.slice(0, authorityEnd);
|
|
701
|
+
if (authority === '' || authority.includes('\\') || authority.includes('@')) {
|
|
702
|
+
defects.push(ProviderOAuth2EndpointTemplateDefect.MALFORMED_AUTHORITY);
|
|
703
|
+
return defects;
|
|
704
|
+
}
|
|
705
|
+
const hostPlaceholders = classifyProviderOAuth2EndpointPlaceholders(template)
|
|
706
|
+
.filter((occurrence) => occurrence.position === ProviderOAuth2EndpointPlaceholderPosition.HOST);
|
|
707
|
+
if (hostPlaceholders.length > 1)
|
|
708
|
+
defects.push(ProviderOAuth2EndpointTemplateDefect.SEVERAL_PLACEHOLDERS_IN_AUTHORITY);
|
|
709
|
+
}
|
|
710
|
+
return defects;
|
|
711
|
+
}
|
|
712
|
+
/** The placeholder syntax an endpoint template uses — the same braces the operation path templates use. */
|
|
713
|
+
export const PROVIDER_OAUTH2_ENDPOINT_PLACEHOLDER_PATTERN = /\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
714
|
+
/**
|
|
715
|
+
* Classify every placeholder in an OAuth2 endpoint template by the URL component it forms.
|
|
716
|
+
*
|
|
717
|
+
* ONE function, shared by the transposer (which decides which placeholders need an allowance) and
|
|
718
|
+
* the runtime (which refuses an unallowed one), so the two cannot disagree about what a host is.
|
|
719
|
+
*
|
|
720
|
+
* Deliberately a string scan, not a URL parse: a template whose host is still `{cloud}` is not a
|
|
721
|
+
* parseable URL, which is exactly the population this classifies.
|
|
722
|
+
*
|
|
723
|
+
* - offset 0 → {@link ProviderOAuth2EndpointPlaceholderPosition.ORIGIN};
|
|
724
|
+
* - between the first `://` and the next `/`, `?` or `#` → HOST;
|
|
725
|
+
* - otherwise → PATH_OR_QUERY.
|
|
726
|
+
*
|
|
727
|
+
* This classification is only meaningful for a template with no
|
|
728
|
+
* {@link findProviderOAuth2EndpointTemplateDefects}: a template that does not begin `http(s)://` or a
|
|
729
|
+
* placeholder has no authority this scan can find, and every consumer must refuse it FIRST rather than
|
|
730
|
+
* trust the PATH_OR_QUERY answer it would get here.
|
|
731
|
+
*/
|
|
732
|
+
export function classifyProviderOAuth2EndpointPlaceholders(template) {
|
|
733
|
+
const schemeSeparator = template.indexOf('://');
|
|
734
|
+
let authorityEnd = -1;
|
|
735
|
+
if (schemeSeparator >= 0) {
|
|
736
|
+
const rest = template.slice(schemeSeparator + 3);
|
|
737
|
+
const end = rest.search(/[/?#]/);
|
|
738
|
+
authorityEnd = end < 0 ? template.length : schemeSeparator + 3 + end;
|
|
739
|
+
}
|
|
740
|
+
const occurrences = [];
|
|
741
|
+
for (const match of template.matchAll(PROVIDER_OAUTH2_ENDPOINT_PLACEHOLDER_PATTERN)) {
|
|
742
|
+
const index = match.index ?? 0;
|
|
743
|
+
const position = index === 0
|
|
744
|
+
? ProviderOAuth2EndpointPlaceholderPosition.ORIGIN
|
|
745
|
+
: schemeSeparator >= 0 && index > schemeSeparator && index < authorityEnd
|
|
746
|
+
? ProviderOAuth2EndpointPlaceholderPosition.HOST
|
|
747
|
+
: ProviderOAuth2EndpointPlaceholderPosition.PATH_OR_QUERY;
|
|
748
|
+
const occupiesWholeHost = position === ProviderOAuth2EndpointPlaceholderPosition.HOST
|
|
749
|
+
&& index === schemeSeparator + 3
|
|
750
|
+
&& index + match[0].length === authorityEnd;
|
|
751
|
+
const followingLiteral = position === ProviderOAuth2EndpointPlaceholderPosition.HOST && !occupiesWholeHost && index === schemeSeparator + 3
|
|
752
|
+
? template.slice(index + match[0].length, authorityEnd)
|
|
753
|
+
: undefined;
|
|
754
|
+
// Only a PURELY literal remainder qualifies: a second placeholder in the authority is refused as malformed anyway.
|
|
755
|
+
const literalHostSuffix = followingLiteral !== undefined && followingLiteral.startsWith('.') && !followingLiteral.includes('{') ? followingLiteral : undefined;
|
|
756
|
+
occurrences.push({ name: match[1], position, index, token: match[0], occupiesWholeHost, ...(literalHostSuffix === undefined ? {} : { literalHostSuffix }) });
|
|
757
|
+
}
|
|
758
|
+
return occurrences;
|
|
759
|
+
}
|
|
57
760
|
/**
|
|
58
761
|
* Vendors whose credential is COMPUTED per request rather than sent as a fixed header.
|
|
59
762
|
*
|