@superdurable/connectors-sdk 0.0.0-stage → 0.1.0

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.
@@ -0,0 +1,236 @@
1
+ import {
2
+ SecretString
3
+ } from "../chunk-KD5DYYSU.js";
4
+ import {
5
+ ReauthorizationRequiredError
6
+ } from "../chunk-SOXYPIMO.js";
7
+
8
+ // src/oauthtoken/index.ts
9
+ import { createPrivateKey, sign } from "crypto";
10
+ var RefreshSkewMs = 3e5;
11
+ var TokenResponse = class {
12
+ /** Replacement access token. */
13
+ accessToken;
14
+ /** Rotated refresh token, empty when the provider retained it. */
15
+ refreshToken;
16
+ /** Access lifetime in milliseconds; zero only when explicitly accepted. */
17
+ expiresInMs;
18
+ /** Refresh token lifetime in milliseconds; zero when omitted. */
19
+ refreshTokenExpiresInMs;
20
+ /** Returned scope string, possibly empty. */
21
+ scope;
22
+ /** Returned token type, possibly empty when explicitly accepted. */
23
+ tokenType;
24
+ /** Only explicitly retained non-standard response fields. */
25
+ retainedFields;
26
+ /** Capture validated provider response fields. */
27
+ constructor(fields) {
28
+ this.accessToken = fields.accessToken;
29
+ this.refreshToken = fields.refreshToken;
30
+ this.expiresInMs = fields.expiresInMs;
31
+ this.refreshTokenExpiresInMs = fields.refreshTokenExpiresInMs;
32
+ this.scope = fields.scope;
33
+ this.tokenType = fields.tokenType;
34
+ this.retainedFields = fields.retainedFields;
35
+ }
36
+ /** Keep the previous refresh token when no rotated value was returned. */
37
+ nextRefreshToken(prior) {
38
+ return this.refreshToken.reveal() ? this.refreshToken : prior;
39
+ }
40
+ };
41
+ var TokenEndpoint = class {
42
+ #options;
43
+ /** Capture endpoint rules without sending a request. */
44
+ constructor(options) {
45
+ this.#options = options;
46
+ }
47
+ /** Exchange a refresh token, preserving terminal-error classification. */
48
+ async exchangeRefreshToken(client, refreshToken, signal) {
49
+ if (!client.id || !refreshToken.reveal()) throw new ReauthorizationRequiredError();
50
+ const parameters = { grant_type: "refresh_token", refresh_token: refreshToken.reveal() };
51
+ const basicClient = this.#addClientAuthentication(parameters, client);
52
+ return this.#exchange(parameters, basicClient, signal);
53
+ }
54
+ /** Obtain an access token with the OAuth client-credentials grant. */
55
+ async exchangeClientCredentials(client, scopes = [], signal) {
56
+ if (!client.id || !client.secret?.reveal()) throw new ReauthorizationRequiredError();
57
+ if (client.authenticationMethod === "none") throw new TypeError(`${this.#options.providerName} client credentials grant needs a confidential client`);
58
+ const parameters = { grant_type: "client_credentials" };
59
+ if (scopes.length) parameters.scope = scopes.join(" ");
60
+ const basicClient = this.#addClientAuthentication(parameters, client);
61
+ return this.#exchange(parameters, basicClient, signal);
62
+ }
63
+ /** Exchange a signed JWT bearer assertion without client credentials. */
64
+ async exchangeJWTBearerAssertion(assertion, signal) {
65
+ if (!assertion.reveal()) throw new ReauthorizationRequiredError();
66
+ return this.#exchange({ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", assertion: assertion.reveal() }, void 0, signal);
67
+ }
68
+ #addClientAuthentication(parameters, client) {
69
+ switch (client.authenticationMethod) {
70
+ case "client_secret_post":
71
+ if (!client.secret?.reveal()) throw new ReauthorizationRequiredError();
72
+ parameters.client_id = client.id;
73
+ parameters.client_secret = client.secret.reveal();
74
+ return void 0;
75
+ case "client_secret_basic":
76
+ if (!client.secret?.reveal()) throw new ReauthorizationRequiredError();
77
+ return client;
78
+ case "none":
79
+ parameters.client_id = client.id;
80
+ return void 0;
81
+ default:
82
+ throw new TypeError(`${this.#options.providerName} OAuth client authentication method is not supported`);
83
+ }
84
+ }
85
+ async #exchange(parameters, basicClient, signal) {
86
+ const options = this.#options;
87
+ validateEndpointURL(options.url, options.allowsPlainHTTP ?? false, options.providerName);
88
+ for (const field of options.retainedResponseFields ?? []) {
89
+ if (standardResponseFields.has(field)) throw new TypeError(`${options.providerName} token endpoint cannot retain the standard ${field} field`);
90
+ }
91
+ for (const [parameter, value] of Object.entries(options.additionalRequestParameters ?? {})) {
92
+ if (grantParameters.has(parameter)) throw new TypeError(`${options.providerName} token endpoint cannot replace the ${parameter} request parameter`);
93
+ parameters[parameter] = value;
94
+ }
95
+ const headers = { Accept: "application/json" };
96
+ let body;
97
+ if (options.usesJSONRequestBody) {
98
+ headers["Content-Type"] = "application/json";
99
+ body = JSON.stringify(parameters);
100
+ } else {
101
+ headers["Content-Type"] = "application/x-www-form-urlencoded";
102
+ body = new URLSearchParams(parameters).toString();
103
+ }
104
+ if (basicClient) headers.Authorization = `Basic ${Buffer.from(`${basicClient.id}:${basicClient.secret?.reveal() ?? ""}`).toString("base64")}`;
105
+ let response;
106
+ try {
107
+ response = await (options.fetch ?? fetch)(options.url, { method: "POST", headers, body, redirect: "manual", ...signal ? { signal } : {} });
108
+ } catch {
109
+ throw new Error(`${options.providerName} token endpoint is unavailable`);
110
+ }
111
+ let contents;
112
+ try {
113
+ contents = await readBoundedBody(response, 1 << 20);
114
+ } catch {
115
+ throw new Error(`${options.providerName} token response could not be read`);
116
+ }
117
+ let decoded;
118
+ try {
119
+ const value = JSON.parse(new TextDecoder().decode(contents));
120
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError();
121
+ decoded = value;
122
+ } catch {
123
+ throw new Error(`${options.providerName} token response is invalid`);
124
+ }
125
+ const errorCode = typeof decoded.error === "string" ? decoded.error : "";
126
+ if (!response.ok || errorCode) {
127
+ if (response.status < 500 && errorCode && options.terminalErrorCodes?.includes(errorCode)) throw new ReauthorizationRequiredError();
128
+ throw new Error(`${options.providerName} token endpoint returned HTTP ${response.status}`);
129
+ }
130
+ const accessToken = decoded.access_token;
131
+ const expiresIn = decoded.expires_in;
132
+ const tokenType = decoded.token_type;
133
+ const hasAcceptedExpiry = Number.isSafeInteger(expiresIn) && expiresIn > 0 || (expiresIn === void 0 || expiresIn === 0) && options.acceptsMissingExpiresIn === true;
134
+ const acceptedTypes = options.acceptedTokenTypes?.length ? options.acceptedTokenTypes : ["Bearer"];
135
+ const hasAcceptedType = typeof tokenType === "string" && acceptedTypes.some((accepted) => accepted.toLowerCase() === tokenType.toLowerCase()) || (tokenType === void 0 || tokenType === "") && options.acceptsMissingTokenType === true;
136
+ if (typeof accessToken !== "string" || !accessToken || !hasAcceptedExpiry || !hasAcceptedType) throw new Error(`${options.providerName} token response omitted required fields`);
137
+ const retainedFields = {};
138
+ for (const field of options.retainedResponseFields ?? []) if (Object.hasOwn(decoded, field)) retainedFields[field] = decoded[field];
139
+ const refreshToken = typeof decoded.refresh_token === "string" ? decoded.refresh_token : "";
140
+ const refreshTokenExpiry = typeof decoded.refresh_token_expires_in === "number" && decoded.refresh_token_expires_in > 0 ? decoded.refresh_token_expires_in : 0;
141
+ return new TokenResponse({ accessToken: new SecretString(accessToken), refreshToken: new SecretString(refreshToken), expiresInMs: (expiresIn ?? 0) * 1e3, refreshTokenExpiresInMs: refreshTokenExpiry * 1e3, scope: typeof decoded.scope === "string" ? decoded.scope : "", tokenType: typeof tokenType === "string" ? tokenType : "", retainedFields });
142
+ }
143
+ };
144
+ var grantParameters = /* @__PURE__ */ new Set(["grant_type", "refresh_token", "client_id", "client_secret", "assertion", "client_assertion", "client_assertion_type"]);
145
+ var standardResponseFields = /* @__PURE__ */ new Set(["access_token", "refresh_token", "expires_in", "refresh_token_expires_in", "scope", "token_type", "error", "error_description", "error_uri", "id_token"]);
146
+ function validateEndpointURL(value, allowsPlainHTTP, providerName) {
147
+ let parsed;
148
+ try {
149
+ parsed = new URL(value);
150
+ } catch {
151
+ throw new TypeError(`${providerName} token endpoint is invalid`);
152
+ }
153
+ if (!parsed.hostname || parsed.protocol !== "https:" && !(allowsPlainHTTP && parsed.protocol === "http:") || parsed.username || parsed.password) throw new TypeError(`${providerName} token endpoint is invalid`);
154
+ }
155
+ async function readBoundedBody(response, maximumBytes) {
156
+ if (!response.body) return new Uint8Array();
157
+ const reader = response.body.getReader();
158
+ const chunks = [];
159
+ let length = 0;
160
+ try {
161
+ for (; ; ) {
162
+ const result = await reader.read();
163
+ if (result.done) break;
164
+ length += result.value.byteLength;
165
+ if (length > maximumBytes) throw new RangeError("token response too large");
166
+ chunks.push(result.value);
167
+ }
168
+ } finally {
169
+ reader.releaseLock();
170
+ }
171
+ const contents = new Uint8Array(length);
172
+ let offset = 0;
173
+ for (const chunk of chunks) {
174
+ contents.set(chunk, offset);
175
+ offset += chunk.byteLength;
176
+ }
177
+ return contents;
178
+ }
179
+ function proportionalRefreshSkewMs(issuedAt, expiresAt) {
180
+ return lifetimeShare(RefreshSkewMs, issuedAt, expiresAt);
181
+ }
182
+ function forcedRefreshFloorMs(issuedAt, expiresAt) {
183
+ return lifetimeShare(6e4, issuedAt, expiresAt);
184
+ }
185
+ function isRefreshRequired(hasAccessToken, expiresAt, now, missingExpiryRule, issuedAt) {
186
+ if (!hasAccessToken) return true;
187
+ if (!expiresAt) return missingExpiryRule !== "keep";
188
+ return expiresAt.getTime() <= now.getTime() + proportionalRefreshSkewMs(issuedAt, expiresAt);
189
+ }
190
+ function isRefreshRequiredWithIssueTime(hasAccessToken, issuedAt, expiresAt, now, missingExpiryRule) {
191
+ return isRefreshRequired(hasAccessToken, expiresAt, now, missingExpiryRule, issuedAt);
192
+ }
193
+ function hasAllScopes(returnedScope, requiredScopes) {
194
+ const granted = new Set(returnedScope.split(/[ ,]+/).filter(Boolean));
195
+ return requiredScopes.every((scope) => granted.has(scope));
196
+ }
197
+ function hasExactScopes(returnedScope, requiredScopes) {
198
+ const granted = new Set(returnedScope.split(/[ ,]+/).filter(Boolean));
199
+ return granted.size === new Set(requiredScopes).size && hasAllScopes(returnedScope, requiredScopes);
200
+ }
201
+ function lifetimeShare(maximumMs, issuedAt, expiresAt) {
202
+ const lifetimeMs = issuedAt && expiresAt ? expiresAt.getTime() - issuedAt.getTime() : 0;
203
+ return lifetimeMs > 0 ? Math.min(maximumMs, lifetimeMs / 4) : maximumMs;
204
+ }
205
+ function parseRSAPrivateKeyPEM(pemText) {
206
+ try {
207
+ const key = createPrivateKey(pemText);
208
+ if (key.asymmetricKeyType !== "rsa") throw new TypeError();
209
+ return key;
210
+ } catch {
211
+ throw new TypeError("private key is invalid or must use RSA");
212
+ }
213
+ }
214
+ function signJWTBearerAssertion(assertion, privateKey) {
215
+ if (!privateKey || privateKey.asymmetricKeyType !== "rsa") throw new TypeError("JWT-bearer assertion private key is missing or invalid");
216
+ if (!assertion.issuer || !assertion.audience || !Number.isFinite(assertion.issuedAt.getTime()) || !Number.isSafeInteger(assertion.lifetimeMs) || assertion.lifetimeMs <= 0) throw new TypeError("JWT-bearer assertion claims are incomplete");
217
+ const claims = { aud: assertion.audience, exp: Math.floor((assertion.issuedAt.getTime() + assertion.lifetimeMs) / 1e3), iat: Math.floor(assertion.issuedAt.getTime() / 1e3), iss: assertion.issuer };
218
+ if (assertion.subject) claims.sub = assertion.subject;
219
+ if (assertion.scope) claims.scope = assertion.scope;
220
+ const unsigned = `${Buffer.from(JSON.stringify({ alg: "RS256", typ: "JWT" })).toString("base64url")}.${Buffer.from(JSON.stringify(claims)).toString("base64url")}`;
221
+ const signature = sign("RSA-SHA256", Buffer.from(unsigned), privateKey).toString("base64url");
222
+ return new SecretString(`${unsigned}.${signature}`);
223
+ }
224
+ export {
225
+ RefreshSkewMs,
226
+ TokenEndpoint,
227
+ TokenResponse,
228
+ forcedRefreshFloorMs,
229
+ hasAllScopes,
230
+ hasExactScopes,
231
+ isRefreshRequired,
232
+ isRefreshRequiredWithIssueTime,
233
+ parseRSAPrivateKeyPEM,
234
+ proportionalRefreshSkewMs,
235
+ signJWTBearerAssertion
236
+ };
@@ -0,0 +1,34 @@
1
+ import { O as OperationRef, f as BranchDefinition, Z as RetryPolicy, C as Call, M as MutationAttempt, Q as QueryAttempt } from './credentials-vrvW0d4j.js';
2
+
3
+ /** Execute options owned by one operation; durations are milliseconds. */
4
+ interface StepDefaults {
5
+ /** Zero or absence uses the Dex method timeout. */ executeMethodTimeoutMs?: number;
6
+ /** Zero or absence uses the Dex heartbeat timeout. */ heartbeatTimeoutMs?: number;
7
+ /** Absence uses the connector one-hour retry policy. */ executeRetry?: RetryPolicy;
8
+ /** Absence inherits the Flow durability. */ executeDurability?: 'default' | 'sync' | 'async';
9
+ }
10
+ /** Stable query routes and Execute defaults. */
11
+ interface QueryDefinition {
12
+ /** Manifest operation identity. */ operation: OperationRef;
13
+ /** Declared routes, including the standard defect route. */ branches: readonly BranchDefinition[];
14
+ /** Execute options for the generated Step. */ stepDefaults: StepDefaults;
15
+ }
16
+ /** Stable mutation routes and Execute defaults. */
17
+ type MutationDefinition = QueryDefinition;
18
+ /** Read-only provider operation invoked once per Dex Execute attempt. */
19
+ interface Query<Input, Output> {
20
+ /** Return an immutable operation definition. */ definition(): QueryDefinition;
21
+ /** Classify one provider call as a branch or safe retry. */ invoke(call: Call, input: Input): QueryAttempt<Output> | Promise<QueryAttempt<Output>>;
22
+ }
23
+ /** Provider write with a stable deduplication key and classified attempt. */
24
+ interface Mutation<Input, Output> {
25
+ /** Return an immutable operation definition. */ definition(): MutationDefinition;
26
+ /** Derive a provider-safe key from the stable call ID and input. */ idempotencyKey(callId: string, input: Input): string;
27
+ /** Classify one provider call as a branch, safe retry, or uncertainty. */ invoke(call: Call, input: Input): MutationAttempt<Output> | Promise<MutationAttempt<Output>>;
28
+ }
29
+ /** Validate a query definition before any provider call or Step registration. */
30
+ declare function validateQueryDefinition(definition: QueryDefinition): void;
31
+ /** Validate a mutation definition before any provider call or Step registration. */
32
+ declare function validateMutationDefinition(definition: MutationDefinition): void;
33
+
34
+ export { type Mutation as M, type QueryDefinition as Q, type StepDefaults as S, type MutationDefinition as a, type Query as b, validateQueryDefinition as c, validateMutationDefinition as v };