@opencoredev/social-sdk 0.2.1 → 0.4.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.
Files changed (63) hide show
  1. package/dist/cli-request.d.ts +15 -0
  2. package/dist/cli-request.js +193 -0
  3. package/dist/cli.d.ts +4 -3
  4. package/dist/cli.js +19 -21
  5. package/dist/cloud/common.d.ts +7 -6
  6. package/dist/cloud/common.js +35 -54
  7. package/dist/cloud/lifecycle.js +31 -35
  8. package/dist/cloud/media.d.ts +2 -2
  9. package/dist/cloud/media.js +13 -3
  10. package/dist/cloud/outcomes.d.ts +4 -3
  11. package/dist/cloud/outcomes.js +8 -15
  12. package/dist/cloud/post-for-me.js +41 -49
  13. package/dist/cloud/zernio.js +58 -98
  14. package/dist/core/client.js +79 -99
  15. package/dist/core/fields.d.ts +14 -0
  16. package/dist/core/fields.js +14 -0
  17. package/dist/core/idempotency.d.ts +7 -2
  18. package/dist/core/idempotency.js +37 -20
  19. package/dist/core/pagination.js +8 -7
  20. package/dist/core/types.d.ts +3 -2
  21. package/dist/platforms/bluesky.d.ts +65 -1
  22. package/dist/platforms/bluesky.js +675 -276
  23. package/dist/platforms/instagram.d.ts +2 -0
  24. package/dist/platforms/instagram.js +130 -105
  25. package/dist/platforms/linkedin.d.ts +58 -1
  26. package/dist/platforms/linkedin.js +877 -107
  27. package/dist/platforms/threads.d.ts +13 -1
  28. package/dist/platforms/threads.js +204 -302
  29. package/dist/platforms/tiktok.d.ts +4 -0
  30. package/dist/platforms/tiktok.js +140 -124
  31. package/dist/platforms/webhook-adapter.d.ts +9 -0
  32. package/dist/platforms/webhook-adapter.js +24 -0
  33. package/dist/platforms/x-engagement.js +7 -12
  34. package/dist/platforms/x-stream.d.ts +83 -0
  35. package/dist/platforms/x-stream.js +350 -0
  36. package/dist/platforms/x.d.ts +87 -0
  37. package/dist/platforms/x.js +648 -120
  38. package/dist/platforms/youtube-upload.d.ts +1 -1
  39. package/dist/platforms/youtube-upload.js +6 -2
  40. package/dist/platforms/youtube.d.ts +28 -4
  41. package/dist/platforms/youtube.js +291 -133
  42. package/dist/server/bluesky-oauth.d.ts +177 -0
  43. package/dist/server/bluesky-oauth.js +1229 -0
  44. package/dist/server/connections.d.ts +14 -0
  45. package/dist/server/connections.js +10 -2
  46. package/dist/server/egress.d.ts +14 -0
  47. package/dist/server/egress.js +115 -0
  48. package/dist/server/oauth-internal.d.ts +6 -0
  49. package/dist/server/oauth-internal.js +66 -0
  50. package/dist/server/oauth.d.ts +1 -1
  51. package/dist/server/oauth.js +46 -99
  52. package/dist/server/webhooks.d.ts +136 -3
  53. package/dist/server/webhooks.js +639 -25
  54. package/dist/testing/index.js +14 -28
  55. package/dist/transport/http.d.ts +1 -1
  56. package/dist/transport/http.js +0 -1
  57. package/dist/transport/json.d.ts +7 -0
  58. package/dist/transport/json.js +32 -4
  59. package/dist/transport/upload.d.ts +1 -1
  60. package/dist/transport/upload.js +46 -38
  61. package/dist/transport/validation.d.ts +16 -5
  62. package/dist/transport/validation.js +29 -7
  63. package/package.json +2 -2
@@ -15,6 +15,10 @@ export interface ConnectionAttempt {
15
15
  }
16
16
  export interface ConnectionStart {
17
17
  readonly authorizationUrl: string;
18
+ /**
19
+ * Public attempt fields. The PKCE verifier always stays in the store, and so
20
+ * does `providerState` when the provider marks it secret.
21
+ */
18
22
  readonly attempt: Omit<ConnectionAttempt, "state" | "codeVerifier"> & {
19
23
  readonly state: string;
20
24
  };
@@ -30,9 +34,17 @@ export interface ConnectionProvider {
30
34
  readonly redirectUri: string;
31
35
  readonly state: string;
32
36
  readonly codeChallenge: string;
37
+ /** Account or server hint typed by the user, such as a Bluesky handle. */
38
+ readonly loginHint?: string;
33
39
  }): Promise<{
34
40
  readonly authorizationUrl: string;
35
41
  readonly providerState?: string;
42
+ /**
43
+ * Set when `providerState` holds secrets, such as a DPoP private key. The
44
+ * manager then keeps it in the store and leaves it out of the attempt
45
+ * returned by `begin`.
46
+ */
47
+ readonly providerStateSecret?: boolean;
36
48
  }>;
37
49
  complete(input: {
38
50
  readonly callbackUrl: string;
@@ -85,6 +97,8 @@ export declare class ConnectionManager {
85
97
  readonly redirectUri: string;
86
98
  readonly allowedRedirectUris: readonly string[];
87
99
  readonly provider: ConnectionProvider;
100
+ /** Passed to the provider unchanged. Bluesky uses it for a handle, DID, or server URL. */
101
+ readonly loginHint?: string;
88
102
  }): Promise<ConnectionStart>;
89
103
  discover(input: {
90
104
  readonly attemptId: string;
@@ -103,13 +103,14 @@ export class ConnectionManager {
103
103
  const codeVerifier = base64Url(this.#options.randomBytes(48));
104
104
  const attemptId = base64Url(this.#options.randomBytes(18));
105
105
  const expiresAt = new Date(now.getTime() + this.#options.ttlMs).toISOString();
106
- const started = await input.provider.start({
106
+ const startInput = {
107
107
  platforms: input.platforms,
108
108
  capabilities: input.capabilities ?? [],
109
109
  redirectUri: input.redirectUri,
110
110
  state,
111
111
  codeChallenge: await challenge(codeVerifier),
112
- });
112
+ };
113
+ const started = await input.provider.start(input.loginHint === undefined ? startInput : { ...startInput, loginHint: input.loginHint });
113
114
  const attemptBase = {
114
115
  id: attemptId,
115
116
  backend: input.backend,
@@ -128,6 +129,13 @@ export class ConnectionManager {
128
129
  : { ...attemptBase, providerState: started.providerState };
129
130
  await this.#options.store.save(attempt);
130
131
  const { state: publicState, codeVerifier: _privateVerifier, ...publicAttempt } = attempt;
132
+ if (started.providerStateSecret === true) {
133
+ const { providerState: _secretProviderState, ...withoutProviderState } = publicAttempt;
134
+ return {
135
+ authorizationUrl: started.authorizationUrl,
136
+ attempt: { ...withoutProviderState, state: publicState },
137
+ };
138
+ }
131
139
  return {
132
140
  authorizationUrl: started.authorizationUrl,
133
141
  attempt: { ...publicAttempt, state: publicState },
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Static egress checks for URLs that come from untrusted identity documents and
3
+ * server metadata. They only look at the URL itself: the scheme, `localhost`
4
+ * names, and IP-literal hosts. A hostname that resolves through DNS to a private
5
+ * address is not caught here, because `fetch` gives no portable way to see or pin
6
+ * the resolved address across Node.js, Bun, and edge runtimes. Server callers
7
+ * that need that guarantee inject a `fetch` that pins DNS results.
8
+ */
9
+ /**
10
+ * Return why a URL must not be fetched, or `undefined` when the static checks
11
+ * pass. Only HTTPS is allowed, `localhost` names are rejected, and IP-literal
12
+ * hosts must be public unicast addresses.
13
+ */
14
+ export declare function egressBlockReason(url: URL): string | undefined;
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Static egress checks for URLs that come from untrusted identity documents and
3
+ * server metadata. They only look at the URL itself: the scheme, `localhost`
4
+ * names, and IP-literal hosts. A hostname that resolves through DNS to a private
5
+ * address is not caught here, because `fetch` gives no portable way to see or pin
6
+ * the resolved address across Node.js, Bun, and edge runtimes. Server callers
7
+ * that need that guarantee inject a `fetch` that pins DNS results.
8
+ */
9
+ /** IPv4 ranges that are never valid public destinations (RFC 6890 and successors). */
10
+ const BLOCKED_V4 = [
11
+ [0x00000000, 8], // 0.0.0.0/8 "this network"
12
+ [0x0a000000, 8], // 10.0.0.0/8 private
13
+ [0x64400000, 10], // 100.64.0.0/10 carrier-grade NAT
14
+ [0x7f000000, 8], // 127.0.0.0/8 loopback
15
+ [0xa9fe0000, 16], // 169.254.0.0/16 link-local
16
+ [0xac100000, 12], // 172.16.0.0/12 private
17
+ [0xc0000000, 24], // 192.0.0.0/24 IETF protocol assignments
18
+ [0xc0000200, 24], // 192.0.2.0/24 documentation
19
+ [0xc0586300, 24], // 192.88.99.0/24 6to4 relay anycast
20
+ [0xc0a80000, 16], // 192.168.0.0/16 private
21
+ [0xc6120000, 15], // 198.18.0.0/15 benchmarking
22
+ [0xc6336400, 24], // 198.51.100.0/24 documentation
23
+ [0xcb007100, 24], // 203.0.113.0/24 documentation
24
+ [0xe0000000, 4], // 224.0.0.0/4 multicast
25
+ [0xf0000000, 4], // 240.0.0.0/4 reserved and broadcast
26
+ ];
27
+ const V4_PATTERN = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
28
+ function parseV4(host) {
29
+ const match = V4_PATTERN.exec(host);
30
+ if (match === null)
31
+ return undefined;
32
+ let value = 0;
33
+ for (const part of match.slice(1)) {
34
+ const octet = Number(part);
35
+ if (octet > 255)
36
+ return undefined;
37
+ value = value * 256 + octet;
38
+ }
39
+ return value;
40
+ }
41
+ function blockedV4(address) {
42
+ return BLOCKED_V4.some(([base, bits]) => {
43
+ const size = 2 ** (32 - bits);
44
+ return address >= base && address < base + size;
45
+ });
46
+ }
47
+ /**
48
+ * Expand an IPv6 literal (without brackets) into eight hextets. The WHATWG URL
49
+ * parser always serializes IPv6 hosts in compressed hexadecimal form, so a
50
+ * dotted IPv4 tail never reaches this function; anything unexpected is rejected.
51
+ */
52
+ function parseV6(host) {
53
+ const halves = host.split("::");
54
+ if (halves.length > 2)
55
+ return undefined;
56
+ const parse = (part) => {
57
+ if (part === undefined || part === "")
58
+ return [];
59
+ const out = [];
60
+ for (const group of part.split(":")) {
61
+ if (!/^[0-9a-f]{1,4}$/i.test(group))
62
+ return undefined;
63
+ out.push(Number.parseInt(group, 16));
64
+ }
65
+ return out;
66
+ };
67
+ const head = parse(halves[0]);
68
+ const rest = halves.length === 2 ? parse(halves[1]) : [];
69
+ if (head === undefined || rest === undefined)
70
+ return undefined;
71
+ const explicit = head.length + rest.length;
72
+ if (halves.length === 1 ? explicit !== 8 : explicit > 7)
73
+ return undefined;
74
+ return [...head, ...Array(8 - explicit).fill(0), ...rest];
75
+ }
76
+ function blockedV6(groups) {
77
+ const [a = 0, b = 0, c = 0, d = 0, e = 0, f = 0, g = 0, h = 0] = groups;
78
+ // IPv4-mapped (::ffff:0:0/96) and NAT64 (64:ff9b::/96): judge the embedded IPv4 address.
79
+ if ((a === 0 && b === 0 && c === 0 && d === 0 && e === 0 && f === 0xffff) ||
80
+ (a === 0x64 && b === 0xff9b && c === 0 && d === 0 && e === 0 && f === 0))
81
+ return blockedV4(g * 0x10000 + h);
82
+ // Only global unicast (2000::/3) can be public. This rejects ::, ::1, fc00::/7,
83
+ // fe80::/10, fec0::/10, ff00::/8, and IPv4-compatible addresses.
84
+ if ((a & 0xe000) !== 0x2000)
85
+ return true;
86
+ return ((a === 0x2001 && b < 0x0200) || // 2001::/23 IETF protocol assignments, including Teredo
87
+ (a === 0x2001 && b === 0x0db8) || // 2001:db8::/32 documentation
88
+ a === 0x2002 || // 2002::/16 6to4, which embeds an arbitrary IPv4 address
89
+ (a === 0x3fff && b < 0x1000) // 3fff::/20 documentation
90
+ );
91
+ }
92
+ /**
93
+ * Return why a URL must not be fetched, or `undefined` when the static checks
94
+ * pass. Only HTTPS is allowed, `localhost` names are rejected, and IP-literal
95
+ * hosts must be public unicast addresses.
96
+ */
97
+ export function egressBlockReason(url) {
98
+ if (url.protocol !== "https:")
99
+ return "only HTTPS URLs are allowed";
100
+ if (url.username || url.password)
101
+ return "URLs with credentials are not allowed";
102
+ const host = url.hostname.toLowerCase().replace(/\.$/, "");
103
+ if (host === "localhost" || host.endsWith(".localhost"))
104
+ return "localhost is not allowed";
105
+ if (host.startsWith("[") && host.endsWith("]")) {
106
+ const groups = parseV6(host.slice(1, -1));
107
+ return groups === undefined || blockedV6(groups)
108
+ ? "non-public IP addresses are not allowed"
109
+ : undefined;
110
+ }
111
+ const v4 = parseV4(host);
112
+ if (v4 !== undefined && blockedV4(v4))
113
+ return "non-public IP addresses are not allowed";
114
+ return undefined;
115
+ }
@@ -0,0 +1,6 @@
1
+ import type { ConnectionAttempt } from "./connections.js";
2
+ export declare function readBounded(response: Response, maxBytes: number, operation: string): Promise<string>;
3
+ export declare function validateCallback(input: {
4
+ readonly callbackUrl: string;
5
+ readonly attempt: ConnectionAttempt;
6
+ }): URL;
@@ -0,0 +1,66 @@
1
+ import { SocialError } from "../core/errors.js";
2
+ // Internal helpers shared by the direct OAuth providers. Not exported from the package.
3
+ function fail(operation, message, code) {
4
+ throw new SocialError({ code, operation, message });
5
+ }
6
+ export async function readBounded(response, maxBytes, operation) {
7
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 1)
8
+ fail("oauth.config", "maxResponseBytes must be a positive safe integer", "invalid_input");
9
+ if (!response.body)
10
+ return "";
11
+ const reader = response.body.getReader();
12
+ const chunks = [];
13
+ let total = 0;
14
+ try {
15
+ for (;;) {
16
+ const part = await reader.read();
17
+ if (part.done)
18
+ break;
19
+ total += part.value.byteLength;
20
+ if (total > maxBytes) {
21
+ await reader.cancel();
22
+ fail(operation, "OAuth provider response exceeded the configured size limit", "upstream_failure");
23
+ }
24
+ chunks.push(part.value);
25
+ }
26
+ }
27
+ finally {
28
+ reader.releaseLock();
29
+ }
30
+ const merged = new Uint8Array(total);
31
+ let offset = 0;
32
+ for (const chunk of chunks) {
33
+ merged.set(chunk, offset);
34
+ offset += chunk.byteLength;
35
+ }
36
+ return new TextDecoder().decode(merged);
37
+ }
38
+ export function validateCallback(input) {
39
+ let callback;
40
+ try {
41
+ callback = new URL(input.callbackUrl);
42
+ }
43
+ catch {
44
+ fail("connections.complete", "OAuth callback URL is invalid", "invalid_input");
45
+ }
46
+ let expected;
47
+ try {
48
+ expected = new URL(input.attempt.redirectUri);
49
+ }
50
+ catch {
51
+ fail("connections.complete", "OAuth attempt redirect URI is invalid", "invalid_input");
52
+ }
53
+ if (callback.origin !== expected.origin ||
54
+ callback.pathname !== expected.pathname ||
55
+ callback.username ||
56
+ callback.password ||
57
+ callback.hash)
58
+ fail("connections.complete", "OAuth callback does not match the registered redirect", "unauthorized");
59
+ for (const [key, value] of expected.searchParams)
60
+ if (callback.searchParams.getAll(key).length !== 1 || callback.searchParams.get(key) !== value)
61
+ fail("connections.complete", "OAuth callback does not match the registered redirect", "unauthorized");
62
+ const states = callback.searchParams.getAll("state");
63
+ if (states.length !== 1 || states[0] !== input.attempt.state)
64
+ fail("connections.complete", "OAuth callback state did not match the authenticated attempt", "unauthorized");
65
+ return callback;
66
+ }
@@ -1,4 +1,5 @@
1
1
  import type { ConnectionAccount, ConnectionAttempt, ConnectionProvider } from "./connections.js";
2
+ export * from "./bluesky-oauth.js";
2
3
  export interface OAuthTokenSet {
3
4
  readonly accessToken: string;
4
5
  readonly refreshToken?: string;
@@ -49,4 +50,3 @@ export declare const tiktokOAuth: (options: OAuthProviderOptions) => ConnectionP
49
50
  export declare const instagramOAuth: (options: OAuthProviderOptions) => ConnectionProvider;
50
51
  export declare const linkedinOAuth: (options: OAuthProviderOptions) => ConnectionProvider;
51
52
  export declare function refreshOAuthToken(kind: ProviderKind, options: OAuthProviderOptions, current: OAuthTokenSet): Promise<OAuthTokenSet>;
52
- export {};
@@ -1,6 +1,10 @@
1
- /* oxlint-disable anti-slop/no-runtime-typeof, anti-slop/no-unknown-parameters, anti-slop/no-unsafe-dictionary-type, anti-slop/require-safety-comment-for-type-assertion, anti-slop/require-readable-spacing -- OAuth responses are unknown by contract and validated at this boundary. */
2
1
  import { SocialError } from "../core/errors.js";
3
- import { connectedAccountRef } from "../core/types.js";
2
+ import { definedFields } from "../core/fields.js";
3
+ import { connectedAccountRef, } from "../core/types.js";
4
+ import { isJsonValue } from "../transport/json.js";
5
+ import { isFiniteNumber, isJsonObject, isString } from "../transport/validation.js";
6
+ import { readBounded, validateCallback } from "./oauth-internal.js";
7
+ export * from "./bluesky-oauth.js";
4
8
  const configs = {
5
9
  youtube: {
6
10
  auth: "https://accounts.google.com/o/oauth2/v2/auth",
@@ -63,17 +67,36 @@ function fail(operation, message, code = "upstream_failure", cause) {
63
67
  throw new SocialError({ code, operation, message, cause });
64
68
  }
65
69
  function asRecord(value, operation = "oauth.response") {
66
- if (typeof value !== "object" || value === null || Array.isArray(value))
70
+ if (!isJsonObject(value))
67
71
  fail(operation, "OAuth provider returned an invalid response");
68
72
  return value;
69
73
  }
70
74
  function requiredString(value, field, operation = "oauth.response") {
71
- if (typeof value !== "string" || value.length === 0 || value.length > 8192)
75
+ if (!isString(value) || value.length === 0 || value.length > 8192)
72
76
  fail(operation, `OAuth provider response is missing ${field}`);
73
77
  return value;
74
78
  }
75
79
  function optionalString(value) {
76
- return typeof value === "string" && value.length > 0 && value.length <= 8192 ? value : undefined;
80
+ return isString(value) && value.length > 0 && value.length <= 8192 ? value : undefined;
81
+ }
82
+ /** Any string, including an empty one, otherwise the fallback. */
83
+ function stringOr(value, fallback) {
84
+ return isString(value) ? value : fallback;
85
+ }
86
+ /**
87
+ * Plain `JSON.parse`, checked against the JSON grammar. OAuth bodies keep native
88
+ * number parsing rather than the lossless integer handling in transport/json.
89
+ */
90
+ function parseJsonText(raw) {
91
+ const parsed = JSON.parse(raw);
92
+ if (!isJsonValue(parsed))
93
+ throw new SyntaxError("Expected a JSON value");
94
+ return parsed;
95
+ }
96
+ function isAbortError(error) {
97
+ if (error instanceof DOMException)
98
+ return error.name === "AbortError";
99
+ return (typeof error === "object" && error !== null && "name" in error && error.name === "AbortError");
77
100
  }
78
101
  function errorForResponse(status, operation, providerCode) {
79
102
  if (status === 400 && providerCode === "invalid_grant")
@@ -97,38 +120,6 @@ function errorForResponse(status, operation, providerCode) {
97
120
  upstreamStatus: status,
98
121
  });
99
122
  }
100
- async function readBounded(response, maxBytes, operation) {
101
- if (!Number.isSafeInteger(maxBytes) || maxBytes < 1)
102
- fail("oauth.config", "maxResponseBytes must be a positive safe integer", "invalid_input");
103
- if (!response.body)
104
- return "";
105
- const reader = response.body.getReader();
106
- const chunks = [];
107
- let total = 0;
108
- try {
109
- for (;;) {
110
- const part = await reader.read();
111
- if (part.done)
112
- break;
113
- total += part.value.byteLength;
114
- if (total > maxBytes) {
115
- await reader.cancel();
116
- fail(operation, "OAuth provider response exceeded the configured size limit", "upstream_failure");
117
- }
118
- chunks.push(part.value);
119
- }
120
- }
121
- finally {
122
- reader.releaseLock();
123
- }
124
- const merged = new Uint8Array(total);
125
- let offset = 0;
126
- for (const chunk of chunks) {
127
- merged.set(chunk, offset);
128
- offset += chunk.byteLength;
129
- }
130
- return new TextDecoder().decode(merged);
131
- }
132
123
  async function body(response, operation, maxBytes) {
133
124
  const raw = await readBounded(response, maxBytes, operation);
134
125
  const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
@@ -136,11 +127,11 @@ async function body(response, operation, maxBytes) {
136
127
  let providerCode;
137
128
  try {
138
129
  const parsed = contentType.includes("json") || raw.trimStart().startsWith("{")
139
- ? JSON.parse(raw)
130
+ ? parseJsonText(raw)
140
131
  : Object.fromEntries(new URLSearchParams(raw).entries());
141
- if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
132
+ if (isJsonObject(parsed)) {
142
133
  const error = parsed["error"];
143
- providerCode = typeof error === "string" ? error : undefined;
134
+ providerCode = isString(error) ? error : undefined;
144
135
  }
145
136
  }
146
137
  catch {
@@ -154,7 +145,7 @@ async function body(response, operation, maxBytes) {
154
145
  contentType.includes("+json") ||
155
146
  raw.trimStart().startsWith("{")) {
156
147
  try {
157
- return asRecord(JSON.parse(raw), operation);
148
+ return asRecord(parseJsonText(raw), operation);
158
149
  }
159
150
  catch {
160
151
  fail(operation, "OAuth provider returned malformed JSON");
@@ -177,9 +168,9 @@ function tokenSet(data, operation = "oauth.token") {
177
168
  const expires = data["expires_in"];
178
169
  let expiresAt;
179
170
  if (expires !== undefined) {
180
- const seconds = typeof expires === "number"
171
+ const seconds = isFiniteNumber(expires)
181
172
  ? expires
182
- : typeof expires === "string" && /^\d+(?:\.\d+)?$/.test(expires)
173
+ : isString(expires) && /^\d+(?:\.\d+)?$/.test(expires)
183
174
  ? Number(expires)
184
175
  : Number.NaN;
185
176
  if (!Number.isFinite(seconds) || seconds < 0 || seconds > 31_536_000_000)
@@ -189,17 +180,7 @@ function tokenSet(data, operation = "oauth.token") {
189
180
  const refreshToken = optionalString(data["refresh_token"]);
190
181
  const scopes = parseScopes(data["scope"]);
191
182
  const tokenType = optionalString(data["token_type"]);
192
- // oxlint-disable-next-line anti-slop/no-known-value-widening -- validated boundary or fixture contract.
193
- const result = { accessToken };
194
- if (refreshToken !== undefined)
195
- result.refreshToken = refreshToken;
196
- if (expiresAt !== undefined)
197
- result.expiresAt = expiresAt;
198
- if (scopes !== undefined)
199
- result.scopes = scopes;
200
- if (tokenType !== undefined)
201
- result.tokenType = tokenType;
202
- return result;
183
+ return { accessToken, ...definedFields({ refreshToken, expiresAt, scopes, tokenType }) };
203
184
  }
204
185
  function tokenResult(data, kind) {
205
186
  let nested = data;
@@ -238,11 +219,7 @@ async function request(fetcher, url, init, operation, options) {
238
219
  catch (error) {
239
220
  if (error instanceof SocialError)
240
221
  throw error;
241
- if ((error instanceof DOMException && error.name === "AbortError") ||
242
- (typeof error === "object" &&
243
- error !== null &&
244
- "name" in error &&
245
- error.name === "AbortError"))
222
+ if (isAbortError(error))
246
223
  fail(operation, "OAuth provider request timed out", "timeout");
247
224
  fail(operation, "OAuth provider request failed", "upstream_failure", error);
248
225
  }
@@ -261,35 +238,6 @@ function validateLinkedInVersion(value) {
261
238
  fail("oauth.config", "LinkedIn OAuth requires an explicit YYYYMM API version", "invalid_input");
262
239
  return value;
263
240
  }
264
- function validateCallback(input) {
265
- let callback;
266
- try {
267
- callback = new URL(input.callbackUrl);
268
- }
269
- catch {
270
- fail("connections.complete", "OAuth callback URL is invalid", "invalid_input");
271
- }
272
- let expected;
273
- try {
274
- expected = new URL(input.attempt.redirectUri);
275
- }
276
- catch {
277
- fail("connections.complete", "OAuth attempt redirect URI is invalid", "invalid_input");
278
- }
279
- if (callback.origin !== expected.origin ||
280
- callback.pathname !== expected.pathname ||
281
- callback.username ||
282
- callback.password ||
283
- callback.hash)
284
- fail("connections.complete", "OAuth callback does not match the registered redirect", "unauthorized");
285
- for (const [key, value] of expected.searchParams)
286
- if (callback.searchParams.getAll(key).length !== 1 || callback.searchParams.get(key) !== value)
287
- fail("connections.complete", "OAuth callback does not match the registered redirect", "unauthorized");
288
- const states = callback.searchParams.getAll("state");
289
- if (states.length !== 1 || states[0] !== input.attempt.state)
290
- fail("connections.complete", "OAuth callback state did not match the authenticated attempt", "unauthorized");
291
- return callback;
292
- }
293
241
  function providerIdentity(kind, hint, discovered) {
294
242
  if (hint === undefined)
295
243
  return;
@@ -416,14 +364,14 @@ async function discover(kind, token, hint, backend, fetcher, options) {
416
364
  return items.map((item) => {
417
365
  const row = asRecord(item, "youtube.account");
418
366
  const snippet = asRecord(row["snippet"] ?? {}, "youtube.account");
419
- return account("youtube", backend, requiredString(row["id"], "channel id", "youtube.account"), typeof snippet["title"] === "string" ? snippet["title"] : "YouTube channel");
367
+ return account("youtube", backend, requiredString(row["id"], "channel id", "youtube.account"), stringOr(snippet["title"], "YouTube channel"));
420
368
  });
421
369
  }
422
370
  if (kind === "x") {
423
371
  const data = await request(fetcher, "https://api.x.com/2/users/me", { headers: auth }, "x.account", options);
424
372
  const row = asRecord(data["data"], "x.account");
425
373
  return [
426
- account("x", backend, requiredString(row["id"], "user id", "x.account"), typeof row["name"] === "string" ? row["name"] : "X account"),
374
+ account("x", backend, requiredString(row["id"], "user id", "x.account"), stringOr(row["name"], "X account")),
427
375
  ];
428
376
  }
429
377
  if (kind === "threads") {
@@ -432,7 +380,7 @@ async function discover(kind, token, hint, backend, fetcher, options) {
432
380
  fail("oauth.config", "Threads API version is invalid", "invalid_input");
433
381
  const data = await request(fetcher, `https://graph.threads.net/${version}/me?fields=id,username`, { headers: auth }, "threads.account", options);
434
382
  return [
435
- account("threads", backend, requiredString(data["id"], "user id", "threads.account"), typeof data["username"] === "string" ? data["username"] : "Threads account"),
383
+ account("threads", backend, requiredString(data["id"], "user id", "threads.account"), stringOr(data["username"], "Threads account")),
436
384
  ];
437
385
  }
438
386
  if (kind === "tiktok") {
@@ -440,7 +388,7 @@ async function discover(kind, token, hint, backend, fetcher, options) {
440
388
  const row = asRecord(data["data"], "tiktok.account");
441
389
  const user = asRecord(row["user"], "tiktok.account");
442
390
  return [
443
- account("tiktok", backend, requiredString(user["open_id"], "open_id", "tiktok.account"), typeof user["display_name"] === "string" ? user["display_name"] : "TikTok account"),
391
+ account("tiktok", backend, requiredString(user["open_id"], "open_id", "tiktok.account"), stringOr(user["display_name"], "TikTok account")),
444
392
  ];
445
393
  }
446
394
  if (kind === "instagram") {
@@ -454,13 +402,13 @@ async function discover(kind, token, hint, backend, fetcher, options) {
454
402
  ? hint
455
403
  : discoveredId;
456
404
  return [
457
- account("instagram", backend, discoveredAccountId, typeof data["username"] === "string" ? data["username"] : "Instagram account"),
405
+ account("instagram", backend, discoveredAccountId, stringOr(data["username"], "Instagram account")),
458
406
  ];
459
407
  }
460
408
  const version = validateLinkedInVersion(options.linkedinApiVersion);
461
409
  const data = await request(fetcher, "https://api.linkedin.com/v2/userinfo", { headers: { ...auth, "LinkedIn-Version": version } }, "linkedin.account", options);
462
410
  const subject = requiredString(data["sub"], "member id", "linkedin.account");
463
- const member = account("linkedin", backend, `urn:li:person:${subject}`, typeof data["name"] === "string" ? data["name"] : "LinkedIn member");
411
+ const member = account("linkedin", backend, `urn:li:person:${subject}`, stringOr(data["name"], "LinkedIn member"));
464
412
  let acl;
465
413
  try {
466
414
  acl = await request(fetcher, "https://api.linkedin.com/rest/organizationAcls?q=roleAssignee", { headers: { ...auth, "LinkedIn-Version": version } }, "linkedin.organizations", options);
@@ -475,11 +423,10 @@ async function discover(kind, token, hint, backend, fetcher, options) {
475
423
  const elements = Array.isArray(acl["elements"]) ? acl["elements"] : [];
476
424
  const organizations = elements.flatMap((entry) => {
477
425
  const row = asRecord(entry, "linkedin.organizations");
478
- const target = typeof row["organizationTarget"] === "string"
479
- ? row["organizationTarget"]
480
- : typeof row["organizationalTarget"] === "string"
481
- ? row["organizationalTarget"]
482
- : "";
426
+ const organizationTarget = row["organizationTarget"];
427
+ const target = isString(organizationTarget)
428
+ ? organizationTarget
429
+ : stringOr(row["organizationalTarget"], "");
483
430
  const role = row["role"];
484
431
  if ((role !== "ADMINISTRATOR" &&
485
432
  role !== "CONTENT_ADMINISTRATOR" &&