@finueva/auth 0.3.0 → 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.
- package/README.md +25 -2
- package/dist/device.d.ts +51 -0
- package/dist/device.js +936 -0
- package/dist/errors-Crd93Rah.d.ts +11 -0
- package/dist/index.d.ts +2 -1
- package/dist/server.d.ts +2 -1
- package/dist/{errors-B0S9-QGH.d.ts → types-zEqO1_r2.d.ts} +1 -11
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -64,14 +64,37 @@ const acceptance = await auth.acceptInvitation({ invitationId });
|
|
|
64
64
|
const credential = await auth.getDriveCredential({
|
|
65
65
|
workspace: { type: "organization", organizationId: organizations[0].id },
|
|
66
66
|
});
|
|
67
|
+
const signOut = await auth.signOut();
|
|
68
|
+
const responseHeaders = new Headers();
|
|
69
|
+
signOut.applyCookies(responseHeaders);
|
|
67
70
|
```
|
|
68
71
|
|
|
69
|
-
The server client is request-scoped and does not cache credentials. It forwards
|
|
72
|
+
The server client is request-scoped and does not cache credentials. It forwards only reserved Better Auth cookies to Auth and never sends them to Drive. Apply cookies returned by server invitation acceptance or sign-out to the outgoing response with `acceptance.applyCookies(headers)` or `signOut.applyCookies(headers)`.
|
|
73
|
+
|
|
74
|
+
## Device
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { createDeviceAuth } from "@finueva/auth/device";
|
|
78
|
+
|
|
79
|
+
const auth = createDeviceAuth({
|
|
80
|
+
authOrigin: "https://auth.finueva.com",
|
|
81
|
+
clientId: "finueva-cli",
|
|
82
|
+
});
|
|
83
|
+
const authorization = await auth.requestAuthorization();
|
|
84
|
+
showCode(authorization.userCode, authorization.verificationUri);
|
|
85
|
+
const token = await auth.pollForToken(authorization, { signal });
|
|
86
|
+
const session = await auth.getSession();
|
|
87
|
+
await auth.revokeCurrentDevice(); // or revokeAllSessions()
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Polling starts at the server-provided interval, adds five seconds after each RFC 8628 `slow_down`, has no transport retry, and stops on denial, expiry, cancellation, the configured `maxPollingMs`, malformed/bounded responses, or transport timeout. The only requested scope is `openid`.
|
|
91
|
+
|
|
92
|
+
The token is memory-only by default. Secure persistence is opt-in through a structural adapter implementing `read`, atomic `replace`, and `delete`; every operation receives an `AbortSignal` and must stop without committing when aborted. The caller owns encryption and platform credential storage. Adapter and authority failures are value-free and never include device codes or tokens. This export uses standard expiring/revocable Better Auth sessions and does not implement refresh tokens.
|
|
70
93
|
|
|
71
94
|
## Current Limits
|
|
72
95
|
|
|
73
96
|
- Organization mutation and active-organization state are not exposed; callers select an organization returned by `listOrganizations()` explicitly.
|
|
74
|
-
- Cross-root SSO is not
|
|
97
|
+
- Cross-root SSO is not implemented.
|
|
75
98
|
- The package does not persist Drive credentials or expose Better Auth or Unstorage internals.
|
|
76
99
|
- Public errors are `AuthError` instances with stable, value-free codes.
|
|
77
100
|
- Organization contexts with more than 50 current teams fail with `organization_context_too_large` and status `422`.
|
package/dist/device.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { o as AuthSession } from "./types-zEqO1_r2.js";
|
|
2
|
+
//#region src/device.d.ts
|
|
3
|
+
type DeviceAuthorizationErrorCode = "device_access_denied" | "device_authorization_cancelled" | "device_code_expired" | "invalid_device_authorization" | "identity_authority_unavailable";
|
|
4
|
+
declare class DeviceAuthorizationError extends Error {
|
|
5
|
+
readonly code: DeviceAuthorizationErrorCode;
|
|
6
|
+
constructor(code: DeviceAuthorizationErrorCode);
|
|
7
|
+
}
|
|
8
|
+
interface DeviceToken {
|
|
9
|
+
accessToken: string;
|
|
10
|
+
tokenType: "Bearer";
|
|
11
|
+
expiresAt: Date;
|
|
12
|
+
scope: "openid";
|
|
13
|
+
}
|
|
14
|
+
interface DeviceTokenPersistence {
|
|
15
|
+
read(signal: AbortSignal): DeviceToken | null | Promise<DeviceToken | null>;
|
|
16
|
+
replace(token: DeviceToken, signal: AbortSignal): void | Promise<void>;
|
|
17
|
+
delete(signal: AbortSignal): void | Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
interface DeviceAuthorization {
|
|
20
|
+
deviceCode: string;
|
|
21
|
+
userCode: string;
|
|
22
|
+
verificationUri: string;
|
|
23
|
+
verificationUriComplete: string;
|
|
24
|
+
expiresAt: Date;
|
|
25
|
+
intervalSeconds: number;
|
|
26
|
+
}
|
|
27
|
+
interface DeviceAuthOptions {
|
|
28
|
+
authOrigin: string;
|
|
29
|
+
clientId: string;
|
|
30
|
+
fetch?: typeof globalThis.fetch;
|
|
31
|
+
now?: () => number;
|
|
32
|
+
persistence?: DeviceTokenPersistence;
|
|
33
|
+
timeoutMs?: number;
|
|
34
|
+
maxPollingMs?: number;
|
|
35
|
+
}
|
|
36
|
+
interface PollDeviceOptions {
|
|
37
|
+
signal?: AbortSignal;
|
|
38
|
+
}
|
|
39
|
+
interface DeviceAuthClient {
|
|
40
|
+
requestAuthorization(options?: {
|
|
41
|
+
signal?: AbortSignal;
|
|
42
|
+
}): Promise<DeviceAuthorization>;
|
|
43
|
+
pollForToken(authorization: DeviceAuthorization, options?: PollDeviceOptions): Promise<DeviceToken>;
|
|
44
|
+
getSession(): Promise<AuthSession | null>;
|
|
45
|
+
getToken(): Promise<DeviceToken | null>;
|
|
46
|
+
revokeCurrentDevice(): Promise<void>;
|
|
47
|
+
revokeAllSessions(): Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
declare function createDeviceAuth(options: DeviceAuthOptions): DeviceAuthClient;
|
|
50
|
+
//#endregion
|
|
51
|
+
export { DeviceAuthClient, DeviceAuthOptions, DeviceAuthorization, DeviceAuthorizationError, DeviceAuthorizationErrorCode, DeviceToken, DeviceTokenPersistence, PollDeviceOptions, createDeviceAuth };
|
package/dist/device.js
ADDED
|
@@ -0,0 +1,936 @@
|
|
|
1
|
+
//#region ../../node_modules/.pnpm/destr@2.0.5/node_modules/destr/dist/index.mjs
|
|
2
|
+
const suspectProtoRx = /"(?:_|\\u0{2}5[Ff]){2}(?:p|\\u0{2}70)(?:r|\\u0{2}72)(?:o|\\u0{2}6[Ff])(?:t|\\u0{2}74)(?:o|\\u0{2}6[Ff])(?:_|\\u0{2}5[Ff]){2}"\s*:/;
|
|
3
|
+
const suspectConstructorRx = /"(?:c|\\u0063)(?:o|\\u006[Ff])(?:n|\\u006[Ee])(?:s|\\u0073)(?:t|\\u0074)(?:r|\\u0072)(?:u|\\u0075)(?:c|\\u0063)(?:t|\\u0074)(?:o|\\u006[Ff])(?:r|\\u0072)"\s*:/;
|
|
4
|
+
const JsonSigRx = /^\s*["[{]|^\s*-?\d{1,16}(\.\d{1,17})?([Ee][+-]?\d+)?\s*$/;
|
|
5
|
+
function jsonParseTransform(key, value) {
|
|
6
|
+
if (key === "__proto__" || key === "constructor" && value && typeof value === "object" && "prototype" in value) {
|
|
7
|
+
warnKeyDropped(key);
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
function warnKeyDropped(key) {
|
|
13
|
+
console.warn(`[destr] Dropping "${key}" key to prevent prototype pollution.`);
|
|
14
|
+
}
|
|
15
|
+
function destr(value, options = {}) {
|
|
16
|
+
if (typeof value !== "string") return value;
|
|
17
|
+
if (value[0] === "\"" && value[value.length - 1] === "\"" && value.indexOf("\\") === -1) return value.slice(1, -1);
|
|
18
|
+
const _value = value.trim();
|
|
19
|
+
if (_value.length <= 9) switch (_value.toLowerCase()) {
|
|
20
|
+
case "true": return true;
|
|
21
|
+
case "false": return false;
|
|
22
|
+
case "undefined": return;
|
|
23
|
+
case "null": return null;
|
|
24
|
+
case "nan": return NaN;
|
|
25
|
+
case "infinity": return Number.POSITIVE_INFINITY;
|
|
26
|
+
case "-infinity": return Number.NEGATIVE_INFINITY;
|
|
27
|
+
}
|
|
28
|
+
if (!JsonSigRx.test(value)) {
|
|
29
|
+
if (options.strict) throw new SyntaxError("[destr] Invalid JSON");
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
if (suspectProtoRx.test(value) || suspectConstructorRx.test(value)) {
|
|
34
|
+
if (options.strict) throw new Error("[destr] Possible prototype pollution");
|
|
35
|
+
return JSON.parse(value, jsonParseTransform);
|
|
36
|
+
}
|
|
37
|
+
return JSON.parse(value);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (options.strict) throw error;
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
String.fromCharCode;
|
|
44
|
+
const HASH_RE = /#/g;
|
|
45
|
+
const AMPERSAND_RE = /&/g;
|
|
46
|
+
const SLASH_RE = /\//g;
|
|
47
|
+
const EQUAL_RE = /=/g;
|
|
48
|
+
const PLUS_RE = /\+/g;
|
|
49
|
+
const ENC_CARET_RE = /%5e/gi;
|
|
50
|
+
const ENC_BACKTICK_RE = /%60/gi;
|
|
51
|
+
const ENC_PIPE_RE = /%7c/gi;
|
|
52
|
+
const ENC_SPACE_RE = /%20/gi;
|
|
53
|
+
function encode(text) {
|
|
54
|
+
return encodeURI("" + text).replace(ENC_PIPE_RE, "|");
|
|
55
|
+
}
|
|
56
|
+
function encodeQueryValue(input) {
|
|
57
|
+
return encode(typeof input === "string" ? input : JSON.stringify(input)).replace(PLUS_RE, "%2B").replace(ENC_SPACE_RE, "+").replace(HASH_RE, "%23").replace(AMPERSAND_RE, "%26").replace(ENC_BACKTICK_RE, "`").replace(ENC_CARET_RE, "^").replace(SLASH_RE, "%2F");
|
|
58
|
+
}
|
|
59
|
+
function encodeQueryKey(text) {
|
|
60
|
+
return encodeQueryValue(text).replace(EQUAL_RE, "%3D");
|
|
61
|
+
}
|
|
62
|
+
function decode(text = "") {
|
|
63
|
+
try {
|
|
64
|
+
return decodeURIComponent("" + text);
|
|
65
|
+
} catch {
|
|
66
|
+
return "" + text;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function decodeQueryKey(text) {
|
|
70
|
+
return decode(text.replace(PLUS_RE, " "));
|
|
71
|
+
}
|
|
72
|
+
function decodeQueryValue(text) {
|
|
73
|
+
return decode(text.replace(PLUS_RE, " "));
|
|
74
|
+
}
|
|
75
|
+
function parseQuery(parametersString = "") {
|
|
76
|
+
const object = Object.create(null);
|
|
77
|
+
if (parametersString[0] === "?") parametersString = parametersString.slice(1);
|
|
78
|
+
for (const parameter of parametersString.split("&")) {
|
|
79
|
+
const s = parameter.match(/([^=]+)=?(.*)/) || [];
|
|
80
|
+
if (s.length < 2) continue;
|
|
81
|
+
const key = decodeQueryKey(s[1]);
|
|
82
|
+
if (key === "__proto__" || key === "constructor") continue;
|
|
83
|
+
const value = decodeQueryValue(s[2] || "");
|
|
84
|
+
if (object[key] === void 0) object[key] = value;
|
|
85
|
+
else if (Array.isArray(object[key])) object[key].push(value);
|
|
86
|
+
else object[key] = [object[key], value];
|
|
87
|
+
}
|
|
88
|
+
return object;
|
|
89
|
+
}
|
|
90
|
+
function encodeQueryItem(key, value) {
|
|
91
|
+
if (typeof value === "number" || typeof value === "boolean") value = String(value);
|
|
92
|
+
if (!value) return encodeQueryKey(key);
|
|
93
|
+
if (Array.isArray(value)) return value.map((_value) => `${encodeQueryKey(key)}=${encodeQueryValue(_value)}`).join("&");
|
|
94
|
+
return `${encodeQueryKey(key)}=${encodeQueryValue(value)}`;
|
|
95
|
+
}
|
|
96
|
+
function stringifyQuery(query) {
|
|
97
|
+
return Object.keys(query).filter((k) => query[k] !== void 0).map((k) => encodeQueryItem(k, query[k])).filter(Boolean).join("&");
|
|
98
|
+
}
|
|
99
|
+
const PROTOCOL_STRICT_REGEX = /^[\s\w\0+.-]{2,}:([/\\]{1,2})/;
|
|
100
|
+
const PROTOCOL_REGEX = /^[\s\w\0+.-]{2,}:([/\\]{2})?/;
|
|
101
|
+
const PROTOCOL_RELATIVE_REGEX = /^([/\\]\s*){2,}[^/\\]/;
|
|
102
|
+
const TRAILING_SLASH_RE = /\/$|\/\?|\/#/;
|
|
103
|
+
const JOIN_LEADING_SLASH_RE = /^\.?\//;
|
|
104
|
+
function hasProtocol(inputString, opts = {}) {
|
|
105
|
+
if (typeof opts === "boolean") opts = { acceptRelative: opts };
|
|
106
|
+
if (opts.strict) return PROTOCOL_STRICT_REGEX.test(inputString);
|
|
107
|
+
return PROTOCOL_REGEX.test(inputString) || (opts.acceptRelative ? PROTOCOL_RELATIVE_REGEX.test(inputString) : false);
|
|
108
|
+
}
|
|
109
|
+
function hasTrailingSlash(input = "", respectQueryAndFragment) {
|
|
110
|
+
if (!respectQueryAndFragment) return input.endsWith("/");
|
|
111
|
+
return TRAILING_SLASH_RE.test(input);
|
|
112
|
+
}
|
|
113
|
+
function withoutTrailingSlash(input = "", respectQueryAndFragment) {
|
|
114
|
+
if (!respectQueryAndFragment) return (hasTrailingSlash(input) ? input.slice(0, -1) : input) || "/";
|
|
115
|
+
if (!hasTrailingSlash(input, true)) return input || "/";
|
|
116
|
+
let path = input;
|
|
117
|
+
let fragment = "";
|
|
118
|
+
const fragmentIndex = input.indexOf("#");
|
|
119
|
+
if (fragmentIndex !== -1) {
|
|
120
|
+
path = input.slice(0, fragmentIndex);
|
|
121
|
+
fragment = input.slice(fragmentIndex);
|
|
122
|
+
}
|
|
123
|
+
const [s0, ...s] = path.split("?");
|
|
124
|
+
return ((s0.endsWith("/") ? s0.slice(0, -1) : s0) || "/") + (s.length > 0 ? `?${s.join("?")}` : "") + fragment;
|
|
125
|
+
}
|
|
126
|
+
function withTrailingSlash(input = "", respectQueryAndFragment) {
|
|
127
|
+
if (!respectQueryAndFragment) return input.endsWith("/") ? input : input + "/";
|
|
128
|
+
if (hasTrailingSlash(input, true)) return input || "/";
|
|
129
|
+
let path = input;
|
|
130
|
+
let fragment = "";
|
|
131
|
+
const fragmentIndex = input.indexOf("#");
|
|
132
|
+
if (fragmentIndex !== -1) {
|
|
133
|
+
path = input.slice(0, fragmentIndex);
|
|
134
|
+
fragment = input.slice(fragmentIndex);
|
|
135
|
+
if (!path) return fragment;
|
|
136
|
+
}
|
|
137
|
+
const [s0, ...s] = path.split("?");
|
|
138
|
+
return s0 + "/" + (s.length > 0 ? `?${s.join("?")}` : "") + fragment;
|
|
139
|
+
}
|
|
140
|
+
function withBase(input, base) {
|
|
141
|
+
if (isEmptyURL(base) || hasProtocol(input)) return input;
|
|
142
|
+
const _base = withoutTrailingSlash(base);
|
|
143
|
+
if (input.startsWith(_base)) {
|
|
144
|
+
const nextChar = input[_base.length];
|
|
145
|
+
if (!nextChar || nextChar === "/" || nextChar === "?") return input;
|
|
146
|
+
}
|
|
147
|
+
return joinURL(_base, input);
|
|
148
|
+
}
|
|
149
|
+
function withQuery(input, query) {
|
|
150
|
+
const parsed = parseURL(input);
|
|
151
|
+
parsed.search = stringifyQuery({
|
|
152
|
+
...parseQuery(parsed.search),
|
|
153
|
+
...query
|
|
154
|
+
});
|
|
155
|
+
return stringifyParsedURL(parsed);
|
|
156
|
+
}
|
|
157
|
+
function isEmptyURL(url) {
|
|
158
|
+
return !url || url === "/";
|
|
159
|
+
}
|
|
160
|
+
function isNonEmptyURL(url) {
|
|
161
|
+
return url && url !== "/";
|
|
162
|
+
}
|
|
163
|
+
function joinURL(base, ...input) {
|
|
164
|
+
let url = base || "";
|
|
165
|
+
for (const segment of input.filter((url2) => isNonEmptyURL(url2))) if (url) {
|
|
166
|
+
const _segment = segment.replace(JOIN_LEADING_SLASH_RE, "");
|
|
167
|
+
url = withTrailingSlash(url) + _segment;
|
|
168
|
+
} else url = segment;
|
|
169
|
+
return url;
|
|
170
|
+
}
|
|
171
|
+
const protocolRelative = Symbol.for("ufo:protocolRelative");
|
|
172
|
+
function parseURL(input = "", defaultProto) {
|
|
173
|
+
const _specialProtoMatch = input.match(/^[\s\0]*(blob:|data:|javascript:|vbscript:)(.*)/i);
|
|
174
|
+
if (_specialProtoMatch) {
|
|
175
|
+
const [, _proto, _pathname = ""] = _specialProtoMatch;
|
|
176
|
+
return {
|
|
177
|
+
protocol: _proto.toLowerCase(),
|
|
178
|
+
pathname: _pathname,
|
|
179
|
+
href: _proto + _pathname,
|
|
180
|
+
auth: "",
|
|
181
|
+
host: "",
|
|
182
|
+
search: "",
|
|
183
|
+
hash: ""
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
if (!hasProtocol(input, { acceptRelative: true })) return defaultProto ? parseURL(defaultProto + input) : parsePath(input);
|
|
187
|
+
const [, protocol = "", auth, hostAndPath = ""] = input.replace(/\\/g, "/").match(/^[\s\0]*([\w+.-]{2,}:)?\/\/([^/@]+@)?(.*)/) || [];
|
|
188
|
+
let [, host = "", path = ""] = hostAndPath.match(/([^#/?]*)(.*)?/) || [];
|
|
189
|
+
if (protocol === "file:") path = path.replace(/\/(?=[A-Za-z]:)/, "");
|
|
190
|
+
const { pathname, search, hash } = parsePath(path);
|
|
191
|
+
return {
|
|
192
|
+
protocol: protocol.toLowerCase(),
|
|
193
|
+
auth: auth ? auth.slice(0, Math.max(0, auth.length - 1)) : "",
|
|
194
|
+
host,
|
|
195
|
+
pathname,
|
|
196
|
+
search,
|
|
197
|
+
hash,
|
|
198
|
+
[protocolRelative]: !protocol
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function parsePath(input = "") {
|
|
202
|
+
const [pathname = "", search = "", hash = ""] = (input.match(/([^#?]*)(\?[^#]*)?(#.*)?/) || []).splice(1);
|
|
203
|
+
return {
|
|
204
|
+
pathname,
|
|
205
|
+
search,
|
|
206
|
+
hash
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function stringifyParsedURL(parsed) {
|
|
210
|
+
const pathname = parsed.pathname || "";
|
|
211
|
+
const search = parsed.search ? (parsed.search.startsWith("?") ? "" : "?") + parsed.search : "";
|
|
212
|
+
const hash = parsed.hash || "";
|
|
213
|
+
const auth = parsed.auth ? parsed.auth + "@" : "";
|
|
214
|
+
const host = parsed.host || "";
|
|
215
|
+
return (parsed.protocol || parsed[protocolRelative] ? (parsed.protocol || "") + "//" : "") + auth + host + pathname + search + hash;
|
|
216
|
+
}
|
|
217
|
+
//#endregion
|
|
218
|
+
//#region ../../node_modules/.pnpm/ofetch@1.5.1/node_modules/ofetch/dist/shared/ofetch.CWycOUEr.mjs
|
|
219
|
+
var FetchError = class extends Error {
|
|
220
|
+
constructor(message, opts) {
|
|
221
|
+
super(message, opts);
|
|
222
|
+
this.name = "FetchError";
|
|
223
|
+
if (opts?.cause && !this.cause) this.cause = opts.cause;
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
function createFetchError(ctx) {
|
|
227
|
+
const errorMessage = ctx.error?.message || ctx.error?.toString() || "";
|
|
228
|
+
const method = ctx.request?.method || ctx.options?.method || "GET";
|
|
229
|
+
const url = ctx.request?.url || String(ctx.request) || "/";
|
|
230
|
+
const fetchError = new FetchError(`${`[${method}] ${JSON.stringify(url)}`}: ${ctx.response ? `${ctx.response.status} ${ctx.response.statusText}` : "<no response>"}${errorMessage ? ` ${errorMessage}` : ""}`, ctx.error ? { cause: ctx.error } : void 0);
|
|
231
|
+
for (const key of [
|
|
232
|
+
"request",
|
|
233
|
+
"options",
|
|
234
|
+
"response"
|
|
235
|
+
]) Object.defineProperty(fetchError, key, { get() {
|
|
236
|
+
return ctx[key];
|
|
237
|
+
} });
|
|
238
|
+
for (const [key, refKey] of [
|
|
239
|
+
["data", "_data"],
|
|
240
|
+
["status", "status"],
|
|
241
|
+
["statusCode", "status"],
|
|
242
|
+
["statusText", "statusText"],
|
|
243
|
+
["statusMessage", "statusText"]
|
|
244
|
+
]) Object.defineProperty(fetchError, key, { get() {
|
|
245
|
+
return ctx.response && ctx.response[refKey];
|
|
246
|
+
} });
|
|
247
|
+
return fetchError;
|
|
248
|
+
}
|
|
249
|
+
const payloadMethods = new Set(Object.freeze([
|
|
250
|
+
"PATCH",
|
|
251
|
+
"POST",
|
|
252
|
+
"PUT",
|
|
253
|
+
"DELETE"
|
|
254
|
+
]));
|
|
255
|
+
function isPayloadMethod(method = "GET") {
|
|
256
|
+
return payloadMethods.has(method.toUpperCase());
|
|
257
|
+
}
|
|
258
|
+
function isJSONSerializable(value) {
|
|
259
|
+
if (value === void 0) return false;
|
|
260
|
+
const t = typeof value;
|
|
261
|
+
if (t === "string" || t === "number" || t === "boolean" || t === null) return true;
|
|
262
|
+
if (t !== "object") return false;
|
|
263
|
+
if (Array.isArray(value)) return true;
|
|
264
|
+
if (value.buffer) return false;
|
|
265
|
+
if (value instanceof FormData || value instanceof URLSearchParams) return false;
|
|
266
|
+
return value.constructor && value.constructor.name === "Object" || typeof value.toJSON === "function";
|
|
267
|
+
}
|
|
268
|
+
const textTypes = new Set([
|
|
269
|
+
"image/svg",
|
|
270
|
+
"application/xml",
|
|
271
|
+
"application/xhtml",
|
|
272
|
+
"application/html"
|
|
273
|
+
]);
|
|
274
|
+
const JSON_RE = /^application\/(?:[\w!#$%&*.^`~-]*\+)?json(;.+)?$/i;
|
|
275
|
+
function detectResponseType(_contentType = "") {
|
|
276
|
+
if (!_contentType) return "json";
|
|
277
|
+
const contentType = _contentType.split(";").shift() || "";
|
|
278
|
+
if (JSON_RE.test(contentType)) return "json";
|
|
279
|
+
if (contentType === "text/event-stream") return "stream";
|
|
280
|
+
if (textTypes.has(contentType) || contentType.startsWith("text/")) return "text";
|
|
281
|
+
return "blob";
|
|
282
|
+
}
|
|
283
|
+
function resolveFetchOptions(request, input, defaults, Headers) {
|
|
284
|
+
const headers = mergeHeaders(input?.headers ?? request?.headers, defaults?.headers, Headers);
|
|
285
|
+
let query;
|
|
286
|
+
if (defaults?.query || defaults?.params || input?.params || input?.query) query = {
|
|
287
|
+
...defaults?.params,
|
|
288
|
+
...defaults?.query,
|
|
289
|
+
...input?.params,
|
|
290
|
+
...input?.query
|
|
291
|
+
};
|
|
292
|
+
return {
|
|
293
|
+
...defaults,
|
|
294
|
+
...input,
|
|
295
|
+
query,
|
|
296
|
+
params: query,
|
|
297
|
+
headers
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
function mergeHeaders(input, defaults, Headers) {
|
|
301
|
+
if (!defaults) return new Headers(input);
|
|
302
|
+
const headers = new Headers(defaults);
|
|
303
|
+
if (input) for (const [key, value] of Symbol.iterator in input || Array.isArray(input) ? input : new Headers(input)) headers.set(key, value);
|
|
304
|
+
return headers;
|
|
305
|
+
}
|
|
306
|
+
async function callHooks(context, hooks) {
|
|
307
|
+
if (hooks) if (Array.isArray(hooks)) for (const hook of hooks) await hook(context);
|
|
308
|
+
else await hooks(context);
|
|
309
|
+
}
|
|
310
|
+
const retryStatusCodes = new Set([
|
|
311
|
+
408,
|
|
312
|
+
409,
|
|
313
|
+
425,
|
|
314
|
+
429,
|
|
315
|
+
500,
|
|
316
|
+
502,
|
|
317
|
+
503,
|
|
318
|
+
504
|
|
319
|
+
]);
|
|
320
|
+
const nullBodyResponses = new Set([
|
|
321
|
+
101,
|
|
322
|
+
204,
|
|
323
|
+
205,
|
|
324
|
+
304
|
|
325
|
+
]);
|
|
326
|
+
function createFetch(globalOptions = {}) {
|
|
327
|
+
const { fetch = globalThis.fetch, Headers = globalThis.Headers, AbortController = globalThis.AbortController } = globalOptions;
|
|
328
|
+
async function onError(context) {
|
|
329
|
+
const isAbort = context.error && context.error.name === "AbortError" && !context.options.timeout || false;
|
|
330
|
+
if (context.options.retry !== false && !isAbort) {
|
|
331
|
+
let retries;
|
|
332
|
+
if (typeof context.options.retry === "number") retries = context.options.retry;
|
|
333
|
+
else retries = isPayloadMethod(context.options.method) ? 0 : 1;
|
|
334
|
+
const responseCode = context.response && context.response.status || 500;
|
|
335
|
+
if (retries > 0 && (Array.isArray(context.options.retryStatusCodes) ? context.options.retryStatusCodes.includes(responseCode) : retryStatusCodes.has(responseCode))) {
|
|
336
|
+
const retryDelay = typeof context.options.retryDelay === "function" ? context.options.retryDelay(context) : context.options.retryDelay || 0;
|
|
337
|
+
if (retryDelay > 0) await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
338
|
+
return $fetchRaw(context.request, {
|
|
339
|
+
...context.options,
|
|
340
|
+
retry: retries - 1
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
const error = createFetchError(context);
|
|
345
|
+
if (Error.captureStackTrace) Error.captureStackTrace(error, $fetchRaw);
|
|
346
|
+
throw error;
|
|
347
|
+
}
|
|
348
|
+
const $fetchRaw = async function $fetchRaw2(_request, _options = {}) {
|
|
349
|
+
const context = {
|
|
350
|
+
request: _request,
|
|
351
|
+
options: resolveFetchOptions(_request, _options, globalOptions.defaults, Headers),
|
|
352
|
+
response: void 0,
|
|
353
|
+
error: void 0
|
|
354
|
+
};
|
|
355
|
+
if (context.options.method) context.options.method = context.options.method.toUpperCase();
|
|
356
|
+
if (context.options.onRequest) {
|
|
357
|
+
await callHooks(context, context.options.onRequest);
|
|
358
|
+
if (!(context.options.headers instanceof Headers)) context.options.headers = new Headers(context.options.headers || {});
|
|
359
|
+
}
|
|
360
|
+
if (typeof context.request === "string") {
|
|
361
|
+
if (context.options.baseURL) context.request = withBase(context.request, context.options.baseURL);
|
|
362
|
+
if (context.options.query) {
|
|
363
|
+
context.request = withQuery(context.request, context.options.query);
|
|
364
|
+
delete context.options.query;
|
|
365
|
+
}
|
|
366
|
+
if ("query" in context.options) delete context.options.query;
|
|
367
|
+
if ("params" in context.options) delete context.options.params;
|
|
368
|
+
}
|
|
369
|
+
if (context.options.body && isPayloadMethod(context.options.method)) {
|
|
370
|
+
if (isJSONSerializable(context.options.body)) {
|
|
371
|
+
const contentType = context.options.headers.get("content-type");
|
|
372
|
+
if (typeof context.options.body !== "string") context.options.body = contentType === "application/x-www-form-urlencoded" ? new URLSearchParams(context.options.body).toString() : JSON.stringify(context.options.body);
|
|
373
|
+
if (!contentType) context.options.headers.set("content-type", "application/json");
|
|
374
|
+
if (!context.options.headers.has("accept")) context.options.headers.set("accept", "application/json");
|
|
375
|
+
} else if ("pipeTo" in context.options.body && typeof context.options.body.pipeTo === "function" || typeof context.options.body.pipe === "function") {
|
|
376
|
+
if (!("duplex" in context.options)) context.options.duplex = "half";
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
let abortTimeout;
|
|
380
|
+
if (!context.options.signal && context.options.timeout) {
|
|
381
|
+
const controller = new AbortController();
|
|
382
|
+
abortTimeout = setTimeout(() => {
|
|
383
|
+
const error = new Error("[TimeoutError]: The operation was aborted due to timeout");
|
|
384
|
+
error.name = "TimeoutError";
|
|
385
|
+
error.code = 23;
|
|
386
|
+
controller.abort(error);
|
|
387
|
+
}, context.options.timeout);
|
|
388
|
+
context.options.signal = controller.signal;
|
|
389
|
+
}
|
|
390
|
+
try {
|
|
391
|
+
context.response = await fetch(context.request, context.options);
|
|
392
|
+
} catch (error) {
|
|
393
|
+
context.error = error;
|
|
394
|
+
if (context.options.onRequestError) await callHooks(context, context.options.onRequestError);
|
|
395
|
+
return await onError(context);
|
|
396
|
+
} finally {
|
|
397
|
+
if (abortTimeout) clearTimeout(abortTimeout);
|
|
398
|
+
}
|
|
399
|
+
if ((context.response.body || context.response._bodyInit) && !nullBodyResponses.has(context.response.status) && context.options.method !== "HEAD") {
|
|
400
|
+
const responseType = (context.options.parseResponse ? "json" : context.options.responseType) || detectResponseType(context.response.headers.get("content-type") || "");
|
|
401
|
+
switch (responseType) {
|
|
402
|
+
case "json": {
|
|
403
|
+
const data = await context.response.text();
|
|
404
|
+
const parseFunction = context.options.parseResponse || destr;
|
|
405
|
+
context.response._data = parseFunction(data);
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
case "stream":
|
|
409
|
+
context.response._data = context.response.body || context.response._bodyInit;
|
|
410
|
+
break;
|
|
411
|
+
default: context.response._data = await context.response[responseType]();
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (context.options.onResponse) await callHooks(context, context.options.onResponse);
|
|
415
|
+
if (!context.options.ignoreResponseError && context.response.status >= 400 && context.response.status < 600) {
|
|
416
|
+
if (context.options.onResponseError) await callHooks(context, context.options.onResponseError);
|
|
417
|
+
return await onError(context);
|
|
418
|
+
}
|
|
419
|
+
return context.response;
|
|
420
|
+
};
|
|
421
|
+
const $fetch = async function $fetch2(request, options) {
|
|
422
|
+
return (await $fetchRaw(request, options))._data;
|
|
423
|
+
};
|
|
424
|
+
$fetch.raw = $fetchRaw;
|
|
425
|
+
$fetch.native = (...args) => fetch(...args);
|
|
426
|
+
$fetch.create = (defaultOptions = {}, customGlobalOptions = {}) => createFetch({
|
|
427
|
+
...globalOptions,
|
|
428
|
+
...customGlobalOptions,
|
|
429
|
+
defaults: {
|
|
430
|
+
...globalOptions.defaults,
|
|
431
|
+
...customGlobalOptions.defaults,
|
|
432
|
+
...defaultOptions
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
return $fetch;
|
|
436
|
+
}
|
|
437
|
+
//#endregion
|
|
438
|
+
//#region src/errors.ts
|
|
439
|
+
const messages = {
|
|
440
|
+
invalid_credentials: "The credentials are invalid.",
|
|
441
|
+
authentication_required: "Authentication is required.",
|
|
442
|
+
request_origin_denied: "The request origin is not allowed.",
|
|
443
|
+
invalid_invitation: "The invitation is invalid.",
|
|
444
|
+
invitation_unavailable: "The invitation is not available.",
|
|
445
|
+
invitation_recipient_mismatch: "The invitation cannot be accepted by this account.",
|
|
446
|
+
email_verification_required: "Email verification is required.",
|
|
447
|
+
invalid_workspace_selection: "The workspace selection is invalid.",
|
|
448
|
+
organization_context_too_large: "The organization context is too large.",
|
|
449
|
+
credential_rate_limited: "Credential requests are temporarily limited.",
|
|
450
|
+
state_conflict: "The authentication state conflicts with the request.",
|
|
451
|
+
access_denied: "Access is denied.",
|
|
452
|
+
identity_authority_unavailable: "Authentication is temporarily unavailable."
|
|
453
|
+
};
|
|
454
|
+
var AuthError = class extends Error {
|
|
455
|
+
code;
|
|
456
|
+
status;
|
|
457
|
+
retryAfterSeconds;
|
|
458
|
+
constructor(code, status, retryAfterSeconds) {
|
|
459
|
+
super(messages[code]);
|
|
460
|
+
this.name = "AuthError";
|
|
461
|
+
this.code = code;
|
|
462
|
+
this.status = status;
|
|
463
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
//#endregion
|
|
467
|
+
//#region src/shared.ts
|
|
468
|
+
const id = /^[A-Za-z0-9_-]{1,128}$/;
|
|
469
|
+
const email = /^[^\s@]+@[^\s@]+$/;
|
|
470
|
+
function normalizeOrigin(value, name) {
|
|
471
|
+
if (value.length === 0 || value.length > 2048 || /[^\x20-\x7e]/.test(value)) throw new TypeError(`${name} must be an ASCII HTTP origin`);
|
|
472
|
+
let url;
|
|
473
|
+
try {
|
|
474
|
+
url = new URL(value);
|
|
475
|
+
} catch {
|
|
476
|
+
throw new TypeError(`${name} must be an ASCII HTTP origin`);
|
|
477
|
+
}
|
|
478
|
+
const local = url.hostname === "localhost" || url.hostname.endsWith(".localhost") || url.hostname === "127.0.0.1";
|
|
479
|
+
if (url.protocol !== "https:" && !(local && url.protocol === "http:") || url.username || url.password || url.pathname !== "/" || url.search || url.hash || url.origin !== value) throw new TypeError(`${name} must be an ASCII HTTP origin`);
|
|
480
|
+
return url.origin;
|
|
481
|
+
}
|
|
482
|
+
function normalizeTimeout(value) {
|
|
483
|
+
const timeout = value ?? 1e4;
|
|
484
|
+
if (!Number.isSafeInteger(timeout) || timeout < 1 || timeout > 6e4) throw new TypeError("timeoutMs must be an integer from 1 to 60000");
|
|
485
|
+
return timeout;
|
|
486
|
+
}
|
|
487
|
+
function normalizeSession(value) {
|
|
488
|
+
if (value === null) return null;
|
|
489
|
+
if (!isObject(value) || !isObject(value.session) || !isObject(value.user)) throw authorityUnavailable();
|
|
490
|
+
const expiresAt = toDate(value.session.expiresAt);
|
|
491
|
+
const image = value.user.image;
|
|
492
|
+
if (!isId(value.session.id) || !expiresAt || !isId(value.user.id) || typeof value.user.name !== "string" || value.user.name.length > 256 || typeof value.user.email !== "string" || value.user.email.length > 320 || !email.test(value.user.email) || typeof value.user.emailVerified !== "boolean" || image !== null && image !== void 0 && (typeof image !== "string" || image.length > 2048)) throw authorityUnavailable();
|
|
493
|
+
return {
|
|
494
|
+
id: value.session.id,
|
|
495
|
+
expiresAt,
|
|
496
|
+
user: {
|
|
497
|
+
id: value.user.id,
|
|
498
|
+
name: value.user.name,
|
|
499
|
+
email: value.user.email,
|
|
500
|
+
emailVerified: value.user.emailVerified,
|
|
501
|
+
image: image ?? null
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
function fromAuthorityError(value, fallbackStatus, retryAfter) {
|
|
506
|
+
const mapped = mapError(isObject(value) && typeof value.code === "string" ? value.code : void 0, fallbackStatus);
|
|
507
|
+
return new AuthError(mapped.code, mapped.status, parseRetryAfter(retryAfter));
|
|
508
|
+
}
|
|
509
|
+
function authorityUnavailable() {
|
|
510
|
+
return new AuthError("identity_authority_unavailable", 503);
|
|
511
|
+
}
|
|
512
|
+
function mapError(code, status) {
|
|
513
|
+
if (code === "authentication_required") return {
|
|
514
|
+
code,
|
|
515
|
+
status: 401
|
|
516
|
+
};
|
|
517
|
+
if (code === "request_origin_denied") return {
|
|
518
|
+
code,
|
|
519
|
+
status: 403
|
|
520
|
+
};
|
|
521
|
+
if (code === "invalid_workspace_selection") return {
|
|
522
|
+
code,
|
|
523
|
+
status: 422
|
|
524
|
+
};
|
|
525
|
+
if (code === "organization_context_too_large") return {
|
|
526
|
+
code,
|
|
527
|
+
status: 422
|
|
528
|
+
};
|
|
529
|
+
if (code === "credential_rate_limited") return {
|
|
530
|
+
code,
|
|
531
|
+
status: 429
|
|
532
|
+
};
|
|
533
|
+
if (code === "identity_authority_unavailable") return {
|
|
534
|
+
code,
|
|
535
|
+
status: 503
|
|
536
|
+
};
|
|
537
|
+
if (status === 401 || status === 400) return {
|
|
538
|
+
code: "invalid_credentials",
|
|
539
|
+
status: 401
|
|
540
|
+
};
|
|
541
|
+
if (status === 403) return {
|
|
542
|
+
code: "access_denied",
|
|
543
|
+
status: 403
|
|
544
|
+
};
|
|
545
|
+
if (status === 409) return {
|
|
546
|
+
code: "state_conflict",
|
|
547
|
+
status: 409
|
|
548
|
+
};
|
|
549
|
+
if (status === 422) return {
|
|
550
|
+
code: "invalid_workspace_selection",
|
|
551
|
+
status: 422
|
|
552
|
+
};
|
|
553
|
+
if (status === 429) return {
|
|
554
|
+
code: "credential_rate_limited",
|
|
555
|
+
status: 429
|
|
556
|
+
};
|
|
557
|
+
return {
|
|
558
|
+
code: "identity_authority_unavailable",
|
|
559
|
+
status: 503
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
function parseRetryAfter(value) {
|
|
563
|
+
if (!value || !/^\d+$/.test(value)) return void 0;
|
|
564
|
+
const seconds = Number(value);
|
|
565
|
+
return Number.isSafeInteger(seconds) && seconds >= 1 && seconds <= 3600 ? seconds : void 0;
|
|
566
|
+
}
|
|
567
|
+
function toDate(value) {
|
|
568
|
+
const result = value instanceof Date ? value : typeof value === "string" ? new Date(value) : void 0;
|
|
569
|
+
return result && Number.isFinite(result.getTime()) ? result : void 0;
|
|
570
|
+
}
|
|
571
|
+
function isId(value) {
|
|
572
|
+
return typeof value === "string" && id.test(value);
|
|
573
|
+
}
|
|
574
|
+
function isObject(value) {
|
|
575
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
576
|
+
}
|
|
577
|
+
//#endregion
|
|
578
|
+
//#region src/http.ts
|
|
579
|
+
const MAX_RESPONSE_BYTES = 64 * 1024;
|
|
580
|
+
async function requestJsonResponse(url, options) {
|
|
581
|
+
let response;
|
|
582
|
+
try {
|
|
583
|
+
response = await createFetch({ fetch: options.fetch }).raw(url, {
|
|
584
|
+
method: options.method,
|
|
585
|
+
headers: options.headers,
|
|
586
|
+
body: options.body,
|
|
587
|
+
credentials: "include",
|
|
588
|
+
retry: 0,
|
|
589
|
+
timeout: options.timeoutMs,
|
|
590
|
+
redirect: "manual",
|
|
591
|
+
signal: options.signal,
|
|
592
|
+
responseType: "stream",
|
|
593
|
+
ignoreResponseError: true
|
|
594
|
+
});
|
|
595
|
+
} catch {
|
|
596
|
+
throw authorityUnavailable();
|
|
597
|
+
}
|
|
598
|
+
const contentType = response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
599
|
+
if (response.status >= 300 || response.status < 200 || contentType !== "application/json") {
|
|
600
|
+
const body = contentType === "application/json" ? await readJson(response) : void 0;
|
|
601
|
+
await response.body?.cancel();
|
|
602
|
+
throw (options.mapError ?? fromAuthorityError)(body, response.status, response.headers.get("retry-after"));
|
|
603
|
+
}
|
|
604
|
+
return {
|
|
605
|
+
value: await readJson(response),
|
|
606
|
+
headers: response.headers
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
async function readJson(response) {
|
|
610
|
+
const length = response.headers.get("content-length");
|
|
611
|
+
if (length && (!/^\d+$/.test(length) || Number(length) > MAX_RESPONSE_BYTES)) {
|
|
612
|
+
await response.body?.cancel();
|
|
613
|
+
throw authorityUnavailable();
|
|
614
|
+
}
|
|
615
|
+
if (!response.body) throw authorityUnavailable();
|
|
616
|
+
const reader = response.body.getReader();
|
|
617
|
+
const chunks = [];
|
|
618
|
+
let size = 0;
|
|
619
|
+
try {
|
|
620
|
+
while (true) {
|
|
621
|
+
const { done, value } = await reader.read();
|
|
622
|
+
if (done) break;
|
|
623
|
+
size += value.byteLength;
|
|
624
|
+
if (size > MAX_RESPONSE_BYTES) throw authorityUnavailable();
|
|
625
|
+
chunks.push(value);
|
|
626
|
+
}
|
|
627
|
+
} catch (error) {
|
|
628
|
+
await reader.cancel();
|
|
629
|
+
throw error;
|
|
630
|
+
} finally {
|
|
631
|
+
reader.releaseLock();
|
|
632
|
+
}
|
|
633
|
+
const bytes = new Uint8Array(size);
|
|
634
|
+
let offset = 0;
|
|
635
|
+
for (const chunk of chunks) {
|
|
636
|
+
bytes.set(chunk, offset);
|
|
637
|
+
offset += chunk.byteLength;
|
|
638
|
+
}
|
|
639
|
+
try {
|
|
640
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
641
|
+
} catch {
|
|
642
|
+
throw authorityUnavailable();
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
//#endregion
|
|
646
|
+
//#region src/device.ts
|
|
647
|
+
const DEVICE_SCOPE = "openid";
|
|
648
|
+
const GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
649
|
+
const MAX_TOKEN_BYTES = 16 * 1024;
|
|
650
|
+
const MAX_POLLING_MS = 900 * 1e3;
|
|
651
|
+
const errorMessages = {
|
|
652
|
+
device_access_denied: "Device authorization was denied.",
|
|
653
|
+
device_authorization_cancelled: "Device authorization was cancelled.",
|
|
654
|
+
device_code_expired: "Device authorization expired.",
|
|
655
|
+
invalid_device_authorization: "Device authorization is invalid.",
|
|
656
|
+
identity_authority_unavailable: "Authentication is temporarily unavailable."
|
|
657
|
+
};
|
|
658
|
+
var DeviceAuthorizationError = class extends Error {
|
|
659
|
+
code;
|
|
660
|
+
constructor(code) {
|
|
661
|
+
super(errorMessages[code]);
|
|
662
|
+
this.name = "DeviceAuthorizationError";
|
|
663
|
+
this.code = code;
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
function createDeviceAuth(options) {
|
|
667
|
+
if (!options || typeof options !== "object") throw new TypeError("device options are required");
|
|
668
|
+
const authOrigin = normalizeOrigin(options.authOrigin, "authOrigin");
|
|
669
|
+
if (!/^[A-Za-z0-9._~-]{1,64}$/.test(options.clientId)) throw new TypeError("clientId is invalid");
|
|
670
|
+
if (options.persistence && !validPersistence(options.persistence)) throw new TypeError("persistence must implement read, replace, and delete");
|
|
671
|
+
const requestFetch = options.fetch ?? globalThis.fetch;
|
|
672
|
+
if (typeof requestFetch !== "function") throw new TypeError("fetch is required");
|
|
673
|
+
const now = options.now ?? Date.now;
|
|
674
|
+
const timeoutMs = normalizeTimeout(options.timeoutMs);
|
|
675
|
+
const maxPollingMs = options.maxPollingMs ?? 600 * 1e3;
|
|
676
|
+
if (!Number.isSafeInteger(maxPollingMs) || maxPollingMs < 1 || maxPollingMs > MAX_POLLING_MS) throw new TypeError("maxPollingMs must be an integer from 1 to 900000");
|
|
677
|
+
let token = null;
|
|
678
|
+
let hydrated = false;
|
|
679
|
+
let hydration = null;
|
|
680
|
+
async function getToken() {
|
|
681
|
+
if (!hydrated) {
|
|
682
|
+
hydration ??= hydrate().finally(() => {
|
|
683
|
+
hydration = null;
|
|
684
|
+
});
|
|
685
|
+
await hydration;
|
|
686
|
+
}
|
|
687
|
+
if (token && token.expiresAt.getTime() <= now()) {
|
|
688
|
+
token = null;
|
|
689
|
+
await removePersisted();
|
|
690
|
+
}
|
|
691
|
+
return token ? copyToken(token) : null;
|
|
692
|
+
}
|
|
693
|
+
async function hydrate() {
|
|
694
|
+
try {
|
|
695
|
+
const loaded = options.persistence ? normalizeToken(await persistenceCall((signal) => options.persistence.read(signal)), now()) : null;
|
|
696
|
+
if (options.persistence && !loaded) await persistenceCall((signal) => options.persistence.delete(signal));
|
|
697
|
+
token = loaded;
|
|
698
|
+
} catch {
|
|
699
|
+
token = null;
|
|
700
|
+
throw unavailable();
|
|
701
|
+
} finally {
|
|
702
|
+
hydrated = true;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
async function requestAuthorization(requestOptions = {}) {
|
|
706
|
+
if (requestOptions.signal?.aborted) throw cancelled();
|
|
707
|
+
try {
|
|
708
|
+
return normalizeAuthorization((await requestJsonResponse(`${authOrigin}/api/v1/auth/device/code`, {
|
|
709
|
+
fetch: requestFetch,
|
|
710
|
+
timeoutMs,
|
|
711
|
+
method: "POST",
|
|
712
|
+
body: {
|
|
713
|
+
client_id: options.clientId,
|
|
714
|
+
scope: DEVICE_SCOPE
|
|
715
|
+
},
|
|
716
|
+
signal: requestOptions.signal,
|
|
717
|
+
mapError: () => invalid()
|
|
718
|
+
})).value, now());
|
|
719
|
+
} catch (error) {
|
|
720
|
+
if (requestOptions.signal?.aborted) throw cancelled();
|
|
721
|
+
if (error instanceof DeviceAuthorizationError) throw error;
|
|
722
|
+
throw unavailable();
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
async function pollForToken(authorization, pollOptions = {}) {
|
|
726
|
+
if (pollOptions.signal?.aborted) throw cancelled();
|
|
727
|
+
const normalized = normalizeAuthorizationInput(authorization, now());
|
|
728
|
+
const startedAt = now();
|
|
729
|
+
const deadline = Math.min(normalized.expiresAt.getTime(), startedAt + maxPollingMs);
|
|
730
|
+
let intervalMs = normalized.intervalSeconds * 1e3;
|
|
731
|
+
while (true) {
|
|
732
|
+
await wait(intervalMs, pollOptions.signal, deadline, now);
|
|
733
|
+
const requestBudget = deadline - now();
|
|
734
|
+
if (requestBudget <= 0) throw expired();
|
|
735
|
+
let response;
|
|
736
|
+
try {
|
|
737
|
+
response = await requestJsonResponse(`${authOrigin}/api/v1/auth/device/token`, {
|
|
738
|
+
fetch: requestFetch,
|
|
739
|
+
timeoutMs: Math.min(timeoutMs, requestBudget),
|
|
740
|
+
method: "POST",
|
|
741
|
+
body: {
|
|
742
|
+
grant_type: GRANT_TYPE,
|
|
743
|
+
device_code: normalized.deviceCode,
|
|
744
|
+
client_id: options.clientId
|
|
745
|
+
},
|
|
746
|
+
signal: pollOptions.signal,
|
|
747
|
+
mapError(value) {
|
|
748
|
+
return new DeviceProtocolError(isObject(value) && typeof value.error === "string" ? value.error : void 0);
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
} catch (error) {
|
|
752
|
+
if (pollOptions.signal?.aborted) throw cancelled();
|
|
753
|
+
if (now() >= deadline) throw expired();
|
|
754
|
+
if (error instanceof DeviceProtocolError) {
|
|
755
|
+
if (error.protocolCode === "authorization_pending") continue;
|
|
756
|
+
if (error.protocolCode === "slow_down") {
|
|
757
|
+
intervalMs += 5e3;
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
if (error.protocolCode === "access_denied") throw new DeviceAuthorizationError("device_access_denied");
|
|
761
|
+
if (error.protocolCode === "expired_token") throw expired();
|
|
762
|
+
if (error.protocolCode === "invalid_grant" || error.protocolCode === "invalid_request") throw invalid();
|
|
763
|
+
}
|
|
764
|
+
throw unavailable();
|
|
765
|
+
}
|
|
766
|
+
if (now() >= deadline) throw expired();
|
|
767
|
+
const next = normalizeTokenResponse(response.value, now());
|
|
768
|
+
if (options.persistence) try {
|
|
769
|
+
await persistenceCall((signal) => options.persistence.replace(copyToken(next), signal));
|
|
770
|
+
} catch {
|
|
771
|
+
token = null;
|
|
772
|
+
hydrated = true;
|
|
773
|
+
await persistenceCall((signal) => options.persistence.delete(signal)).catch(() => void 0);
|
|
774
|
+
throw unavailable();
|
|
775
|
+
}
|
|
776
|
+
token = next;
|
|
777
|
+
hydrated = true;
|
|
778
|
+
return copyToken(next);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
async function authenticated(path, body) {
|
|
782
|
+
const current = await getToken();
|
|
783
|
+
if (!current) return null;
|
|
784
|
+
try {
|
|
785
|
+
return await requestJsonResponse(`${authOrigin}/api/v1/auth/${path}`, {
|
|
786
|
+
fetch: requestFetch,
|
|
787
|
+
timeoutMs,
|
|
788
|
+
method: body ? "POST" : "GET",
|
|
789
|
+
headers: { authorization: `Bearer ${current.accessToken}` },
|
|
790
|
+
body
|
|
791
|
+
});
|
|
792
|
+
} catch {
|
|
793
|
+
throw unavailable();
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
async function revoke(path) {
|
|
797
|
+
const current = await getToken();
|
|
798
|
+
if (!current) return;
|
|
799
|
+
try {
|
|
800
|
+
await requestJsonResponse(`${authOrigin}/api/v1/auth/${path}`, {
|
|
801
|
+
fetch: requestFetch,
|
|
802
|
+
timeoutMs,
|
|
803
|
+
method: "POST",
|
|
804
|
+
headers: { authorization: `Bearer ${current.accessToken}` },
|
|
805
|
+
body: path === "revoke-session" ? { token: current.accessToken } : void 0
|
|
806
|
+
});
|
|
807
|
+
} catch {
|
|
808
|
+
throw unavailable();
|
|
809
|
+
}
|
|
810
|
+
token = null;
|
|
811
|
+
await removePersisted();
|
|
812
|
+
}
|
|
813
|
+
async function removePersisted() {
|
|
814
|
+
if (!options.persistence) return;
|
|
815
|
+
try {
|
|
816
|
+
await persistenceCall((signal) => options.persistence.delete(signal));
|
|
817
|
+
} catch {
|
|
818
|
+
throw unavailable();
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
return {
|
|
822
|
+
requestAuthorization,
|
|
823
|
+
pollForToken,
|
|
824
|
+
getToken,
|
|
825
|
+
async getSession() {
|
|
826
|
+
const response = await authenticated("get-session");
|
|
827
|
+
return response ? normalizeSession(response.value) : null;
|
|
828
|
+
},
|
|
829
|
+
revokeCurrentDevice: () => revoke("revoke-session"),
|
|
830
|
+
revokeAllSessions: () => revoke("revoke-sessions")
|
|
831
|
+
};
|
|
832
|
+
async function persistenceCall(operation) {
|
|
833
|
+
const controller = new AbortController();
|
|
834
|
+
let timer;
|
|
835
|
+
try {
|
|
836
|
+
return await Promise.race([Promise.resolve().then(() => operation(controller.signal)), new Promise((_, reject) => {
|
|
837
|
+
timer = setTimeout(() => {
|
|
838
|
+
controller.abort();
|
|
839
|
+
reject(unavailable());
|
|
840
|
+
}, Math.min(timeoutMs, 1e3));
|
|
841
|
+
})]);
|
|
842
|
+
} finally {
|
|
843
|
+
if (timer) clearTimeout(timer);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
var DeviceProtocolError = class extends Error {
|
|
848
|
+
protocolCode;
|
|
849
|
+
constructor(protocolCode) {
|
|
850
|
+
super("Device authorization request failed");
|
|
851
|
+
this.protocolCode = protocolCode;
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
function normalizeAuthorization(value, currentTime) {
|
|
855
|
+
if (!isObject(value) || !boundedSecret(value.device_code) || typeof value.user_code !== "string" || !/^[A-Z0-9-]{1,32}$/.test(value.user_code) || !exactHttpsOrLocalUri(value.verification_uri) || !exactHttpsOrLocalUri(value.verification_uri_complete) || !Number.isSafeInteger(value.expires_in) || value.expires_in < 1 || value.expires_in > 900 || !Number.isSafeInteger(value.interval) || value.interval < 1 || value.interval > 60) throw unavailable();
|
|
856
|
+
return {
|
|
857
|
+
deviceCode: value.device_code,
|
|
858
|
+
userCode: value.user_code,
|
|
859
|
+
verificationUri: value.verification_uri,
|
|
860
|
+
verificationUriComplete: value.verification_uri_complete,
|
|
861
|
+
expiresAt: new Date(currentTime + value.expires_in * 1e3),
|
|
862
|
+
intervalSeconds: value.interval
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
function normalizeAuthorizationInput(value, currentTime) {
|
|
866
|
+
if (!value || !boundedSecret(value.deviceCode) || typeof value.userCode !== "string" || !exactHttpsOrLocalUri(value.verificationUri) || !exactHttpsOrLocalUri(value.verificationUriComplete) || !(value.expiresAt instanceof Date) || !Number.isFinite(value.expiresAt.getTime()) || value.expiresAt.getTime() <= currentTime || !Number.isSafeInteger(value.intervalSeconds) || value.intervalSeconds < 1 || value.intervalSeconds > 60) throw invalid();
|
|
867
|
+
return value;
|
|
868
|
+
}
|
|
869
|
+
function normalizeTokenResponse(value, currentTime) {
|
|
870
|
+
if (!isObject(value) || !boundedSecret(value.access_token) || value.token_type !== "Bearer" || value.scope !== DEVICE_SCOPE || !Number.isSafeInteger(value.expires_in) || value.expires_in < 1 || value.expires_in > 744 * 60 * 60) throw unavailable();
|
|
871
|
+
return {
|
|
872
|
+
accessToken: value.access_token,
|
|
873
|
+
tokenType: "Bearer",
|
|
874
|
+
expiresAt: new Date(currentTime + value.expires_in * 1e3),
|
|
875
|
+
scope: DEVICE_SCOPE
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
function normalizeToken(value, currentTime) {
|
|
879
|
+
if (value === null) return null;
|
|
880
|
+
if (!value || !boundedSecret(value.accessToken) || value.tokenType !== "Bearer" || value.scope !== DEVICE_SCOPE || !(value.expiresAt instanceof Date) || !Number.isFinite(value.expiresAt.getTime())) throw unavailable();
|
|
881
|
+
return value.expiresAt.getTime() > currentTime ? copyToken(value) : null;
|
|
882
|
+
}
|
|
883
|
+
function copyToken(value) {
|
|
884
|
+
return {
|
|
885
|
+
...value,
|
|
886
|
+
expiresAt: new Date(value.expiresAt)
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
function boundedSecret(value) {
|
|
890
|
+
return typeof value === "string" && value.length > 0 && new TextEncoder().encode(value).byteLength <= MAX_TOKEN_BYTES;
|
|
891
|
+
}
|
|
892
|
+
function exactHttpsOrLocalUri(value) {
|
|
893
|
+
if (typeof value !== "string" || value.length > 2048 || /[^\x20-\x7e]/.test(value)) return false;
|
|
894
|
+
try {
|
|
895
|
+
const url = new URL(value);
|
|
896
|
+
const local = url.hostname === "localhost" || url.hostname.endsWith(".localhost") || url.hostname === "127.0.0.1";
|
|
897
|
+
return !url.username && !url.password && (url.protocol === "https:" || local && url.protocol === "http:");
|
|
898
|
+
} catch {
|
|
899
|
+
return false;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
function validPersistence(value) {
|
|
903
|
+
return typeof value.read === "function" && typeof value.replace === "function" && typeof value.delete === "function";
|
|
904
|
+
}
|
|
905
|
+
async function wait(milliseconds, signal, deadline, now) {
|
|
906
|
+
if (signal?.aborted) throw cancelled();
|
|
907
|
+
const remaining = deadline - now();
|
|
908
|
+
if (remaining <= 0 || milliseconds > remaining) throw expired();
|
|
909
|
+
await new Promise((resolve, reject) => {
|
|
910
|
+
const timeout = setTimeout(done, milliseconds);
|
|
911
|
+
function done() {
|
|
912
|
+
signal?.removeEventListener("abort", abort);
|
|
913
|
+
resolve();
|
|
914
|
+
}
|
|
915
|
+
function abort() {
|
|
916
|
+
clearTimeout(timeout);
|
|
917
|
+
signal?.removeEventListener("abort", abort);
|
|
918
|
+
reject(cancelled());
|
|
919
|
+
}
|
|
920
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
function invalid() {
|
|
924
|
+
return new DeviceAuthorizationError("invalid_device_authorization");
|
|
925
|
+
}
|
|
926
|
+
function expired() {
|
|
927
|
+
return new DeviceAuthorizationError("device_code_expired");
|
|
928
|
+
}
|
|
929
|
+
function cancelled() {
|
|
930
|
+
return new DeviceAuthorizationError("device_authorization_cancelled");
|
|
931
|
+
}
|
|
932
|
+
function unavailable() {
|
|
933
|
+
return new DeviceAuthorizationError("identity_authority_unavailable");
|
|
934
|
+
}
|
|
935
|
+
//#endregion
|
|
936
|
+
export { DeviceAuthorizationError, createDeviceAuth };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region src/errors.d.ts
|
|
2
|
+
type AuthErrorCode = "invalid_credentials" | "authentication_required" | "request_origin_denied" | "invalid_invitation" | "invitation_unavailable" | "invitation_recipient_mismatch" | "email_verification_required" | "invalid_workspace_selection" | "organization_context_too_large" | "credential_rate_limited" | "state_conflict" | "access_denied" | "identity_authority_unavailable";
|
|
3
|
+
type AuthErrorStatus = 401 | 403 | 409 | 422 | 429 | 503;
|
|
4
|
+
declare class AuthError extends Error {
|
|
5
|
+
readonly code: AuthErrorCode;
|
|
6
|
+
readonly status: AuthErrorStatus;
|
|
7
|
+
readonly retryAfterSeconds?: number;
|
|
8
|
+
constructor(code: AuthErrorCode, status: AuthErrorStatus, retryAfterSeconds?: number);
|
|
9
|
+
}
|
|
10
|
+
//#endregion
|
|
11
|
+
export { AuthErrorCode as n, AuthErrorStatus as r, AuthError as t };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as AuthPersistenceDriver, c as AuthUser, d as FinuevaAuthClient, f as InvitationAcceptance, g as WorkspaceSelection, h as SignInInput, i as AuthPersistence, l as DriveCredential, m as PersonalWorkspaceSelection, n as AuthClientOptions, o as AuthSession, p as OrganizationWorkspaceSelection, r as AuthOrganization, s as AuthState, t as AcceptInvitationInput, u as DriveCredentialInput } from "./types-zEqO1_r2.js";
|
|
2
|
+
import { n as AuthErrorCode, r as AuthErrorStatus, t as AuthError } from "./errors-Crd93Rah.js";
|
|
2
3
|
//#region src/client.d.ts
|
|
3
4
|
declare function createAuthClient(options: AuthClientOptions): FinuevaAuthClient;
|
|
4
5
|
//#endregion
|
package/dist/server.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { c as AuthUser, f as InvitationAcceptance, g as WorkspaceSelection, l as DriveCredential, m as PersonalWorkspaceSelection, o as AuthSession, p as OrganizationWorkspaceSelection, r as AuthOrganization, t as AcceptInvitationInput, u as DriveCredentialInput } from "./types-zEqO1_r2.js";
|
|
2
|
+
import { n as AuthErrorCode, r as AuthErrorStatus, t as AuthError } from "./errors-Crd93Rah.js";
|
|
2
3
|
//#region src/server.d.ts
|
|
3
4
|
interface ServerAuthOptions {
|
|
4
5
|
authOrigin: string;
|
|
@@ -87,14 +87,4 @@ interface FinuevaAuthClient {
|
|
|
87
87
|
onSessionChange(listener: (session: AuthSession | null) => void): () => void;
|
|
88
88
|
}
|
|
89
89
|
//#endregion
|
|
90
|
-
|
|
91
|
-
type AuthErrorCode = "invalid_credentials" | "authentication_required" | "request_origin_denied" | "invalid_invitation" | "invitation_unavailable" | "invitation_recipient_mismatch" | "email_verification_required" | "invalid_workspace_selection" | "organization_context_too_large" | "credential_rate_limited" | "state_conflict" | "access_denied" | "identity_authority_unavailable";
|
|
92
|
-
type AuthErrorStatus = 401 | 403 | 409 | 422 | 429 | 503;
|
|
93
|
-
declare class AuthError extends Error {
|
|
94
|
-
readonly code: AuthErrorCode;
|
|
95
|
-
readonly status: AuthErrorStatus;
|
|
96
|
-
readonly retryAfterSeconds?: number;
|
|
97
|
-
constructor(code: AuthErrorCode, status: AuthErrorStatus, retryAfterSeconds?: number);
|
|
98
|
-
}
|
|
99
|
-
//#endregion
|
|
100
|
-
export { PersonalWorkspaceSelection as _, AuthClientOptions as a, AuthPersistenceDriver as c, AuthUser as d, DriveCredential as f, OrganizationWorkspaceSelection as g, InvitationAcceptance as h, AcceptInvitationInput as i, AuthSession as l, FinuevaAuthClient as m, AuthErrorCode as n, AuthOrganization as o, DriveCredentialInput as p, AuthErrorStatus as r, AuthPersistence as s, AuthError as t, AuthState as u, SignInInput as v, WorkspaceSelection as y };
|
|
90
|
+
export { AuthPersistenceDriver as a, AuthUser as c, FinuevaAuthClient as d, InvitationAcceptance as f, WorkspaceSelection as g, SignInInput as h, AuthPersistence as i, DriveCredential as l, PersonalWorkspaceSelection as m, AuthClientOptions as n, AuthSession as o, OrganizationWorkspaceSelection as p, AuthOrganization as r, AuthState as s, AcceptInvitationInput as t, DriveCredentialInput as u };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@finueva/auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Public browser and server SDK for Finueva Auth",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
"./server": {
|
|
19
19
|
"types": "./dist/server.d.ts",
|
|
20
20
|
"import": "./dist/server.js"
|
|
21
|
+
},
|
|
22
|
+
"./device": {
|
|
23
|
+
"types": "./dist/device.d.ts",
|
|
24
|
+
"import": "./dist/device.js"
|
|
21
25
|
}
|
|
22
26
|
},
|
|
23
27
|
"files": [
|
|
@@ -38,6 +42,7 @@
|
|
|
38
42
|
"coverage": "vitest run --coverage",
|
|
39
43
|
"lint": "eslint .",
|
|
40
44
|
"test": "pnpm test:unit && pnpm build && pnpm test:package",
|
|
45
|
+
"test:composition": "node scripts/test-composition.mjs",
|
|
41
46
|
"test:package": "node scripts/test-package.mjs",
|
|
42
47
|
"test:unit": "vitest run",
|
|
43
48
|
"typecheck": "tsc --noEmit"
|