@apifuse/provider-sdk 2.2.0-beta.20 → 2.2.0-beta.21
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/CHANGELOG.md +5 -0
- package/dist/ceremonies/index.d.ts +8 -0
- package/dist/ceremonies/index.js +57 -0
- package/dist/define.js +103 -1
- package/dist/index.d.ts +1 -1
- package/dist/lint.d.ts +1 -1
- package/dist/provider.d.ts +1 -1
- package/dist/runtime/auth-flow.d.ts +1 -0
- package/dist/runtime/auth-flow.js +1 -0
- package/dist/runtime/state.js +223 -22
- package/dist/server/serve.d.ts +9 -8
- package/dist/server/serve.js +7 -1
- package/dist/server/types.d.ts +4 -0
- package/dist/server/types.js +7 -1
- package/dist/testing/run.js +8 -2
- package/dist/types.d.ts +28 -12
- package/package.json +1 -1
- package/src/ceremonies/index.ts +79 -0
- package/src/define.ts +138 -1
- package/src/index.ts +1 -0
- package/src/lint.ts +7 -1
- package/src/provider.ts +1 -0
- package/src/runtime/auth-flow.ts +2 -0
- package/src/runtime/state.ts +317 -28
- package/src/server/serve.ts +20 -14
- package/src/server/types.ts +7 -1
- package/src/testing/run.ts +8 -2
- package/src/types.ts +48 -26
package/dist/server/serve.d.ts
CHANGED
|
@@ -37,6 +37,7 @@ export declare const ProviderServerStatefulForwardEnvelopeSchema: z.ZodObject<{
|
|
|
37
37
|
credentials: "credentials";
|
|
38
38
|
oauth2: "oauth2";
|
|
39
39
|
"platform-managed": "platform-managed";
|
|
40
|
+
oauth2_proxied: "oauth2_proxied";
|
|
40
41
|
}>;
|
|
41
42
|
secrets: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
42
43
|
scopes: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -142,14 +143,14 @@ export type ProviderServerOptions = {
|
|
|
142
143
|
* @example
|
|
143
144
|
* ```ts
|
|
144
145
|
* await serve(provider, {
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
146
|
+
* shutdown: {
|
|
147
|
+
* hooks: [
|
|
148
|
+
* async () => { await emitter.flush(); },
|
|
149
|
+
* async () => { await sessionManager.closeAll("server-shutdown"); },
|
|
150
|
+
* async () => { await lease.release(); },
|
|
151
|
+
* async () => { await router.close(); },
|
|
152
|
+
* ],
|
|
153
|
+
* },
|
|
153
154
|
* });
|
|
154
155
|
* ```
|
|
155
156
|
*/
|
package/dist/server/serve.js
CHANGED
|
@@ -296,6 +296,7 @@ function createAuthFlowContext(provider, request, options = {}, signal) {
|
|
|
296
296
|
: undefined;
|
|
297
297
|
return {
|
|
298
298
|
context: {
|
|
299
|
+
flowId: request.flowId,
|
|
299
300
|
connectionId: request.connectionId,
|
|
300
301
|
externalRef: request.externalRef,
|
|
301
302
|
tenantId: request.tenantId ?? "",
|
|
@@ -318,7 +319,12 @@ function createAuthFlowContext(provider, request, options = {}, signal) {
|
|
|
318
319
|
},
|
|
319
320
|
}
|
|
320
321
|
: {}),
|
|
321
|
-
env: createEnvContext(
|
|
322
|
+
env: createEnvContext([
|
|
323
|
+
...(provider.secrets?.map((secret) => secret.name) ?? []),
|
|
324
|
+
...(provider.auth?.mode === "oauth2_proxied"
|
|
325
|
+
? ["APIFUSE__AUTH_PROXY__URL"]
|
|
326
|
+
: []),
|
|
327
|
+
]),
|
|
322
328
|
credential,
|
|
323
329
|
context: flowContextStore.context,
|
|
324
330
|
stt: options.stt ?? createSttClientFromEnv(provider.stt),
|
package/dist/server/types.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export declare const ConnectionModeSchema: z.ZodEnum<{
|
|
|
4
4
|
credentials: "credentials";
|
|
5
5
|
oauth2: "oauth2";
|
|
6
6
|
"platform-managed": "platform-managed";
|
|
7
|
+
oauth2_proxied: "oauth2_proxied";
|
|
7
8
|
}>;
|
|
8
9
|
export declare const OperationConnectionSchema: z.ZodObject<{
|
|
9
10
|
id: z.ZodString;
|
|
@@ -12,6 +13,7 @@ export declare const OperationConnectionSchema: z.ZodObject<{
|
|
|
12
13
|
credentials: "credentials";
|
|
13
14
|
oauth2: "oauth2";
|
|
14
15
|
"platform-managed": "platform-managed";
|
|
16
|
+
oauth2_proxied: "oauth2_proxied";
|
|
15
17
|
}>;
|
|
16
18
|
secrets: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
17
19
|
scopes: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -29,6 +31,7 @@ export declare const OperationRequestSchema: z.ZodObject<{
|
|
|
29
31
|
credentials: "credentials";
|
|
30
32
|
oauth2: "oauth2";
|
|
31
33
|
"platform-managed": "platform-managed";
|
|
34
|
+
oauth2_proxied: "oauth2_proxied";
|
|
32
35
|
}>;
|
|
33
36
|
secrets: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
34
37
|
scopes: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -119,6 +122,7 @@ export declare const AuthFlowRequestSchema: z.ZodObject<{
|
|
|
119
122
|
credentials: "credentials";
|
|
120
123
|
oauth2: "oauth2";
|
|
121
124
|
"platform-managed": "platform-managed";
|
|
125
|
+
oauth2_proxied: "oauth2_proxied";
|
|
122
126
|
}>;
|
|
123
127
|
secrets: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
124
128
|
scopes: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
package/dist/server/types.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { PROVIDER_ERROR_SOURCES } from "../observability.js";
|
|
3
3
|
import { HttpRetryPreset } from "../types.js";
|
|
4
|
-
export const ConnectionModeSchema = z.enum([
|
|
4
|
+
export const ConnectionModeSchema = z.enum([
|
|
5
|
+
"oauth2",
|
|
6
|
+
"oauth2_proxied",
|
|
7
|
+
"credentials",
|
|
8
|
+
"platform-managed",
|
|
9
|
+
"none",
|
|
10
|
+
]);
|
|
5
11
|
export const OperationConnectionSchema = z.object({
|
|
6
12
|
id: z.string(),
|
|
7
13
|
mode: ConnectionModeSchema,
|
package/dist/testing/run.js
CHANGED
|
@@ -11,7 +11,13 @@ import { findStreamCaptureGroup, replayStreamEvidence } from "../stream-evidence
|
|
|
11
11
|
// A single lowercase segment (no hyphen) is a valid id, so the trailing group
|
|
12
12
|
// is optional (`*`), matching providers like `kakaomap`, `kstartup`, `triple`.
|
|
13
13
|
const CONNECTOR_ID_REGEX = /^[a-z][a-z0-9]*(-[a-z][a-z0-9]*)*$/;
|
|
14
|
-
const VALID_AUTH_MODES = [
|
|
14
|
+
const VALID_AUTH_MODES = [
|
|
15
|
+
"none",
|
|
16
|
+
"platform-managed",
|
|
17
|
+
"credentials",
|
|
18
|
+
"oauth2",
|
|
19
|
+
"oauth2_proxied",
|
|
20
|
+
];
|
|
15
21
|
const UPDATE_SNAPSHOT_ARGS = new Set(["-u", "--update-snapshots"]);
|
|
16
22
|
const snapshotCaptureStates = new WeakMap();
|
|
17
23
|
function isFixtureEnvelope(value) {
|
|
@@ -556,7 +562,7 @@ function formatJsonDiff(current, expected) {
|
|
|
556
562
|
const expectedLine = expectedLines[index];
|
|
557
563
|
if (currentLine === expectedLine) {
|
|
558
564
|
if (currentLine !== undefined) {
|
|
559
|
-
lines.push(`
|
|
565
|
+
lines.push(` ${currentLine}`);
|
|
560
566
|
}
|
|
561
567
|
continue;
|
|
562
568
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -47,6 +47,8 @@ export interface OperationToolRouterMetadata {
|
|
|
47
47
|
riskClass?: OperationRiskClass;
|
|
48
48
|
/** OpenAI remote-MCP approval hint. Defaults from riskClass. */
|
|
49
49
|
approval?: OperationApprovalPolicy;
|
|
50
|
+
/** Canonical operation-level connection requirement consumed by the registry and Gateway. */
|
|
51
|
+
connectionMode?: "none" | "optional" | "required";
|
|
50
52
|
/** Override connection requirement when provider auth + openWorld inference is insufficient. */
|
|
51
53
|
requiresConnection?: boolean;
|
|
52
54
|
/** Public argument used to resolve the tenant-owned connection. Defaults to externalRef. */
|
|
@@ -648,7 +650,7 @@ export interface StealthProfile {
|
|
|
648
650
|
h2Settings?: Record<string, unknown>;
|
|
649
651
|
headerOrder?: string[];
|
|
650
652
|
}
|
|
651
|
-
export type AuthMode = "none" | "platform-managed" | "credentials" | "oauth2";
|
|
653
|
+
export type AuthMode = "none" | "platform-managed" | "credentials" | "oauth2" | "oauth2_proxied";
|
|
652
654
|
export type ConnectionMode = AuthMode;
|
|
653
655
|
export type ProviderReviewed = "first-party" | "community" | "staging";
|
|
654
656
|
export type ProviderAccessVisibility = "public" | "early_access";
|
|
@@ -658,18 +660,18 @@ export type ProviderProxyMode = "disabled" | "optional" | "required";
|
|
|
658
660
|
* (a common mistake because the names collide with a well-known rebrand):
|
|
659
661
|
*
|
|
660
662
|
* - `smartproxy` — **api.smartproxy.org**, a residential proxy with an IP
|
|
661
|
-
*
|
|
662
|
-
*
|
|
663
|
-
*
|
|
663
|
+
* *extraction/allocation* API (app_key → a pool of raw `ip:port` CONNECT
|
|
664
|
+
* endpoints). This is our own vendor. It is NOT the company formerly named
|
|
665
|
+
* "Smartproxy". Credentials: `APIFUSE__PROXY__SMARTPROXY_APP_KEY`.
|
|
664
666
|
* - `nodemaven` — **gate.nodemaven.com**, a *gateway* proxy with static
|
|
665
|
-
*
|
|
667
|
+
* credentials; geo/session encoded in the username, no allocation API.
|
|
666
668
|
* - `decodo` — **decodo.com**, the *gateway* proxy that was named "Smartproxy"
|
|
667
|
-
*
|
|
668
|
-
*
|
|
669
|
-
*
|
|
670
|
-
*
|
|
669
|
+
* (smartproxy.com) before its 2025 rebrand to Decodo. Sticky sessions via
|
|
670
|
+
* username params. A different company from `smartproxy` above.
|
|
671
|
+
* **@deprecated** — unused; no managed adapter. Use `smartproxy`/`nodemaven`,
|
|
672
|
+
* or the `APIFUSE__PROXY__URL` bring-your-own escape hatch.
|
|
671
673
|
* - `custom` — **@deprecated** bring-your-own static proxy URL marker. The
|
|
672
|
-
*
|
|
674
|
+
* `APIFUSE__PROXY__URL` env still works without declaring this value.
|
|
673
675
|
*/
|
|
674
676
|
export type ProviderProxyProvider = "smartproxy" | "nodemaven" | "decodo" | "custom";
|
|
675
677
|
export type ProviderProxySessionAffinity = "request" | "operation" | "auth-flow" | "connection";
|
|
@@ -715,9 +717,9 @@ export interface ProviderAccessConfig {
|
|
|
715
717
|
* Provider-level rollout visibility.
|
|
716
718
|
*
|
|
717
719
|
* - `public`: visible in public docs/catalog/OpenAPI and callable through
|
|
718
|
-
*
|
|
720
|
+
* the existing provider policy stack.
|
|
719
721
|
* - `early_access`: hidden from public discovery and callable only when the
|
|
720
|
-
*
|
|
722
|
+
* active customer organization has a provider-level access grant.
|
|
721
723
|
*
|
|
722
724
|
* This is intentionally provider-level only. It does not alter auth mode,
|
|
723
725
|
* operation schemas, health-check authoring, `openWorld`, or Connection
|
|
@@ -1528,6 +1530,8 @@ export interface AuthFlowTerminalContext {
|
|
|
1528
1530
|
}): AuthTurn;
|
|
1529
1531
|
}
|
|
1530
1532
|
export interface FlowContext {
|
|
1533
|
+
/** Gateway auth-flow id. Required by flow-scoped auth ceremonies. */
|
|
1534
|
+
flowId?: string;
|
|
1531
1535
|
connectionId?: string;
|
|
1532
1536
|
externalRef?: string;
|
|
1533
1537
|
tenantId: string;
|
|
@@ -1633,8 +1637,20 @@ export interface ProviderContext {
|
|
|
1633
1637
|
stt: SttContext;
|
|
1634
1638
|
choice: ProviderChoiceContext;
|
|
1635
1639
|
}
|
|
1640
|
+
export interface ProxiedOAuthConfig {
|
|
1641
|
+
authorizeUrl: string;
|
|
1642
|
+
tokenUrl: string;
|
|
1643
|
+
customScheme: string;
|
|
1644
|
+
rewriteProfile: string;
|
|
1645
|
+
clientIdEnvKey: string;
|
|
1646
|
+
pkce?: "S256" | "none";
|
|
1647
|
+
authorizeParams?: Record<string, string>;
|
|
1648
|
+
tokenParams?: Record<string, string>;
|
|
1649
|
+
}
|
|
1636
1650
|
export interface AuthConfig {
|
|
1637
1651
|
mode: AuthMode;
|
|
1652
|
+
/** Browser reverse-proxy contract for providers with custom-scheme OAuth callbacks. */
|
|
1653
|
+
proxied?: ProxiedOAuthConfig;
|
|
1638
1654
|
flow?: AuthFlowDefinition;
|
|
1639
1655
|
}
|
|
1640
1656
|
export interface ProviderSecretDeclaration {
|
package/package.json
CHANGED
package/src/ceremonies/index.ts
CHANGED
|
@@ -26,6 +26,15 @@ const validateAuthTurn = ajv.compile(AUTH_TURN_SCHEMA);
|
|
|
26
26
|
|
|
27
27
|
const OAUTH2_STATE_KEY = "__oauth2_state";
|
|
28
28
|
const OAUTH2_PKCE_VERIFIER_KEY = "__oauth2_pkce_verifier";
|
|
29
|
+
export const OAUTH2_PROXIED_AUTH_PROXY_ORIGIN_ENV_KEY = "APIFUSE__AUTH_PROXY__URL";
|
|
30
|
+
export const OAUTH2_PROXIED_PKCE_VERIFIER_KEY = "__oauth2_proxied_pkce_verifier";
|
|
31
|
+
const OAUTH2_PROXIED_RESERVED_AUTHORIZE_PARAMS = new Set([
|
|
32
|
+
"client_id",
|
|
33
|
+
"response_type",
|
|
34
|
+
"state",
|
|
35
|
+
"code_challenge",
|
|
36
|
+
"code_challenge_method",
|
|
37
|
+
]);
|
|
29
38
|
const DEVICE_FLOW_KEY = "__device_flow";
|
|
30
39
|
const MAGIC_LINK_KEY = "__magic_link";
|
|
31
40
|
const COMBINED_STAGE_KEY = "__combined_stage";
|
|
@@ -248,6 +257,76 @@ export function createOAuth2Ceremony(options: {
|
|
|
248
257
|
};
|
|
249
258
|
}
|
|
250
259
|
|
|
260
|
+
/**
|
|
261
|
+
* Builds the start handler for a custom-scheme OAuth provider. The shared
|
|
262
|
+
* auth-proxy owns state minting and callback capture, while the provider owns
|
|
263
|
+
* token exchange and any following authentication turns.
|
|
264
|
+
*/
|
|
265
|
+
export function createOAuth2ProxiedStart(
|
|
266
|
+
options: import("../types.js").ProxiedOAuthConfig,
|
|
267
|
+
): import("../types.js").AuthFlowStartHandler {
|
|
268
|
+
const pkce = options.pkce ?? "S256";
|
|
269
|
+
|
|
270
|
+
return (ctx) =>
|
|
271
|
+
runCeremonyHandler(
|
|
272
|
+
async () => {
|
|
273
|
+
for (const key of Object.keys(options.authorizeParams ?? {})) {
|
|
274
|
+
if (OAUTH2_PROXIED_RESERVED_AUTHORIZE_PARAMS.has(key.toLowerCase())) {
|
|
275
|
+
throw new ValidationError(
|
|
276
|
+
`OAuth2 proxied authorizeParams cannot override reserved parameter "${key}".`,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const proxyOrigin = ctx.env.get(OAUTH2_PROXIED_AUTH_PROXY_ORIGIN_ENV_KEY);
|
|
281
|
+
if (!proxyOrigin) {
|
|
282
|
+
throw new ProviderSecretError(
|
|
283
|
+
`Missing required platform environment: ${OAUTH2_PROXIED_AUTH_PROXY_ORIGIN_ENV_KEY}`,
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
const clientId = getRequiredEnv(ctx, options.clientIdEnvKey);
|
|
287
|
+
if (!ctx.flowId) {
|
|
288
|
+
throw new ValidationError(
|
|
289
|
+
"OAuth2 proxied start requires the gateway flow id.",
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const authorizePath = new URL(options.authorizeUrl).pathname;
|
|
294
|
+
const url = new URL(
|
|
295
|
+
`/p/${encodeURIComponent(ctx.providerId)}/f/${encodeURIComponent(ctx.flowId)}${authorizePath}`,
|
|
296
|
+
proxyOrigin,
|
|
297
|
+
);
|
|
298
|
+
url.searchParams.set("client_id", clientId);
|
|
299
|
+
url.searchParams.set("response_type", "code");
|
|
300
|
+
|
|
301
|
+
if (pkce === "S256") {
|
|
302
|
+
const verifier = createCodeVerifier();
|
|
303
|
+
ctx.context.set(OAUTH2_PROXIED_PKCE_VERIFIER_KEY, verifier);
|
|
304
|
+
url.searchParams.set("code_challenge", createCodeChallenge(verifier));
|
|
305
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
for (const [key, value] of Object.entries(options.authorizeParams ?? {})) {
|
|
309
|
+
url.searchParams.set(key, value);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return createTurn("redirect", {
|
|
313
|
+
data: { url: url.toString() },
|
|
314
|
+
hint: "Open the provider authorization page to continue.",
|
|
315
|
+
expectedInput: {
|
|
316
|
+
type: "object",
|
|
317
|
+
required: ["code", "state"],
|
|
318
|
+
properties: {
|
|
319
|
+
code: { type: "string" },
|
|
320
|
+
state: { type: "string" },
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
},
|
|
325
|
+
"OAuth2 proxied start failed",
|
|
326
|
+
ctx,
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
251
330
|
export function createDeviceFlowCeremony(options: {
|
|
252
331
|
deviceCodeUrl: string;
|
|
253
332
|
tokenUrl: string;
|
package/src/define.ts
CHANGED
|
@@ -84,7 +84,36 @@ interface ProviderImplementationProfile {
|
|
|
84
84
|
const CONNECTOR_ID_REGEX = /^[a-z][a-z0-9]*(-[a-z][a-z0-9]*)*$/;
|
|
85
85
|
const OPERATION_ID_REGEX = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/;
|
|
86
86
|
const VALID_RUNTIMES = ["standard", "shared", "browser"] as const;
|
|
87
|
-
const VALID_AUTH_MODES = [
|
|
87
|
+
const VALID_AUTH_MODES = [
|
|
88
|
+
"none",
|
|
89
|
+
"platform-managed",
|
|
90
|
+
"credentials",
|
|
91
|
+
"oauth2",
|
|
92
|
+
"oauth2_proxied",
|
|
93
|
+
] as const;
|
|
94
|
+
const PROXIED_OAUTH_REQUIRED_FIELDS = [
|
|
95
|
+
"authorizeUrl",
|
|
96
|
+
"tokenUrl",
|
|
97
|
+
"customScheme",
|
|
98
|
+
"rewriteProfile",
|
|
99
|
+
"clientIdEnvKey",
|
|
100
|
+
] as const;
|
|
101
|
+
const PROXIED_OAUTH_ALLOWED_FIELDS = new Set([
|
|
102
|
+
...PROXIED_OAUTH_REQUIRED_FIELDS,
|
|
103
|
+
"pkce",
|
|
104
|
+
"authorizeParams",
|
|
105
|
+
"tokenParams",
|
|
106
|
+
]);
|
|
107
|
+
const PROXIED_OAUTH_RESERVED_AUTHORIZE_PARAMS = new Set([
|
|
108
|
+
"client_id",
|
|
109
|
+
"response_type",
|
|
110
|
+
"state",
|
|
111
|
+
"code_challenge",
|
|
112
|
+
"code_challenge_method",
|
|
113
|
+
]);
|
|
114
|
+
const PROXIED_OAUTH_PROFILE_REGEX = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
115
|
+
const PROXIED_OAUTH_ENV_KEY_REGEX = /^[A-Z][A-Z0-9_]*__[A-Z0-9_]+$/;
|
|
116
|
+
const CUSTOM_SCHEME_REGEX = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/\S+$/;
|
|
88
117
|
const VALID_PROVIDER_ACCESS_VISIBILITIES = ["public", "early_access"] as const;
|
|
89
118
|
const VALID_PROVIDER_PROXY_MODES = ["disabled", "optional", "required"] as const;
|
|
90
119
|
const VALID_PROVIDER_PROXY_PROVIDERS = ["smartproxy", "nodemaven", "decodo", "custom"] as const;
|
|
@@ -303,6 +332,111 @@ function assertLiteralField<TValue extends string>(
|
|
|
303
332
|
);
|
|
304
333
|
}
|
|
305
334
|
}
|
|
335
|
+
|
|
336
|
+
function validateProxiedOAuthParams(
|
|
337
|
+
value: unknown,
|
|
338
|
+
field: "authorizeParams" | "tokenParams",
|
|
339
|
+
providerId: string,
|
|
340
|
+
): void {
|
|
341
|
+
if (value === undefined) return;
|
|
342
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
343
|
+
throw new ValidationError(
|
|
344
|
+
`Provider "${providerId}" auth.proxied.${field} must be an object of string values.`,
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
for (const [key, paramValue] of Object.entries(value)) {
|
|
348
|
+
if (!key.trim() || typeof paramValue !== "string") {
|
|
349
|
+
throw new ValidationError(
|
|
350
|
+
`Provider "${providerId}" auth.proxied.${field} must contain non-empty keys and string values.`,
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
if (
|
|
354
|
+
field === "authorizeParams" &&
|
|
355
|
+
PROXIED_OAUTH_RESERVED_AUTHORIZE_PARAMS.has(key.toLowerCase())
|
|
356
|
+
) {
|
|
357
|
+
throw new ValidationError(
|
|
358
|
+
`Provider "${providerId}" auth.proxied.authorizeParams cannot override reserved parameter "${key}".`,
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function validateProxiedOAuthAuth(auth: Record<string, unknown>, providerId: string): void {
|
|
365
|
+
const proxied = auth.proxied;
|
|
366
|
+
if (auth.mode !== "oauth2_proxied") {
|
|
367
|
+
if (proxied !== undefined) {
|
|
368
|
+
throw new ValidationError(
|
|
369
|
+
`Provider "${providerId}" auth.proxied is only valid when auth.mode is "oauth2_proxied".`,
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (!proxied || typeof proxied !== "object" || Array.isArray(proxied)) {
|
|
375
|
+
throw new ValidationError(
|
|
376
|
+
`Provider "${providerId}" with auth.mode "oauth2_proxied" must declare auth.proxied.`,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
const config = Object.fromEntries(Object.entries(proxied));
|
|
380
|
+
for (const key of Object.keys(config)) {
|
|
381
|
+
if (!PROXIED_OAUTH_ALLOWED_FIELDS.has(key)) {
|
|
382
|
+
throw new ValidationError(
|
|
383
|
+
`Provider "${providerId}" has unknown auth.proxied field "${key}".`,
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
for (const field of PROXIED_OAUTH_REQUIRED_FIELDS) {
|
|
388
|
+
if (typeof config[field] !== "string" || !config[field].trim()) {
|
|
389
|
+
throw new ValidationError(
|
|
390
|
+
`Provider "${providerId}" auth.proxied.${field} must be a non-empty string.`,
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
for (const field of ["authorizeUrl", "tokenUrl"] as const) {
|
|
395
|
+
try {
|
|
396
|
+
const endpoint = new URL(String(config[field]));
|
|
397
|
+
if (
|
|
398
|
+
endpoint.protocol !== "https:" ||
|
|
399
|
+
endpoint.username ||
|
|
400
|
+
endpoint.password ||
|
|
401
|
+
endpoint.hash
|
|
402
|
+
) {
|
|
403
|
+
throw new Error("invalid endpoint");
|
|
404
|
+
}
|
|
405
|
+
} catch {
|
|
406
|
+
throw new ValidationError(
|
|
407
|
+
`Provider "${providerId}" auth.proxied.${field} must be an absolute HTTPS URL without credentials or a fragment.`,
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
const customScheme = String(config.customScheme);
|
|
412
|
+
if (
|
|
413
|
+
!CUSTOM_SCHEME_REGEX.test(customScheme) ||
|
|
414
|
+
customScheme.toLowerCase().startsWith("http://") ||
|
|
415
|
+
customScheme.toLowerCase().startsWith("https://")
|
|
416
|
+
) {
|
|
417
|
+
throw new ValidationError(
|
|
418
|
+
`Provider "${providerId}" auth.proxied.customScheme must be a non-HTTP custom-scheme URL prefix.`,
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
if (!PROXIED_OAUTH_PROFILE_REGEX.test(String(config.rewriteProfile))) {
|
|
422
|
+
throw new ValidationError(
|
|
423
|
+
`Provider "${providerId}" auth.proxied.rewriteProfile must be a kebab-case profile name.`,
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
if (!PROXIED_OAUTH_ENV_KEY_REGEX.test(String(config.clientIdEnvKey))) {
|
|
427
|
+
throw new ValidationError(
|
|
428
|
+
`Provider "${providerId}" auth.proxied.clientIdEnvKey must be an APIFuse-style uppercase environment key.`,
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
if (config.pkce !== undefined && config.pkce !== "S256" && config.pkce !== "none") {
|
|
432
|
+
throw new ValidationError(
|
|
433
|
+
`Provider "${providerId}" auth.proxied.pkce must be "S256" or "none".`,
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
validateProxiedOAuthParams(config.authorizeParams, "authorizeParams", providerId);
|
|
437
|
+
validateProxiedOAuthParams(config.tokenParams, "tokenParams", providerId);
|
|
438
|
+
}
|
|
439
|
+
|
|
306
440
|
function validateProviderShape(config: unknown): void {
|
|
307
441
|
assertObjectConfig(config);
|
|
308
442
|
assertRequiredField(config, "id");
|
|
@@ -315,6 +449,9 @@ function validateProviderShape(config: unknown): void {
|
|
|
315
449
|
const auth = config.auth;
|
|
316
450
|
if (auth && typeof auth === "object" && "mode" in auth && typeof auth.mode === "string")
|
|
317
451
|
assertLiteralField(auth.mode, "auth.mode", VALID_AUTH_MODES, String(config.id));
|
|
452
|
+
if (auth && typeof auth === "object" && !Array.isArray(auth)) {
|
|
453
|
+
validateProxiedOAuthAuth(Object.fromEntries(Object.entries(auth)), String(config.id));
|
|
454
|
+
}
|
|
318
455
|
if (auth && typeof auth === "object" && "exchange" in auth) {
|
|
319
456
|
throw new ProviderError(
|
|
320
457
|
`Provider "${String(config.id)}" auth.exchange is not part of the Provider SDK auth contract`,
|
package/src/index.ts
CHANGED
package/src/lint.ts
CHANGED
|
@@ -3,7 +3,13 @@ import type { ZodType } from "zod";
|
|
|
3
3
|
import { lintPublicSchemaFieldNames } from "./public-schema-field-lint.js";
|
|
4
4
|
import { APIFUSE_DESCRIPTION_KEY_META_KEY, APIFUSE_SENSITIVE_META_KEY } from "./schema.js";
|
|
5
5
|
|
|
6
|
-
type AuthModeLike =
|
|
6
|
+
type AuthModeLike =
|
|
7
|
+
| "none"
|
|
8
|
+
| "platform-managed"
|
|
9
|
+
| "credentials"
|
|
10
|
+
| "oauth2"
|
|
11
|
+
| "oauth2_proxied"
|
|
12
|
+
| "api-key";
|
|
7
13
|
|
|
8
14
|
type ProviderAuthLike = {
|
|
9
15
|
mode?: AuthModeLike;
|
package/src/provider.ts
CHANGED
package/src/runtime/auth-flow.ts
CHANGED
|
@@ -48,6 +48,7 @@ export function createScratchpad(
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
export function createFlowContext(options: {
|
|
51
|
+
flowId?: string;
|
|
51
52
|
http: HttpClient;
|
|
52
53
|
stealth: StealthClient;
|
|
53
54
|
env: EnvContext;
|
|
@@ -60,6 +61,7 @@ export function createFlowContext(options: {
|
|
|
60
61
|
stt?: SttContext;
|
|
61
62
|
}): FlowContext {
|
|
62
63
|
return {
|
|
64
|
+
flowId: options.flowId,
|
|
63
65
|
connectionId: options.connectionId,
|
|
64
66
|
externalRef: options.externalRef,
|
|
65
67
|
tenantId: options.tenantId,
|