@bskyprism/atproto-oauth-client-cloudflare-workers 0.2.2
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/LICENSE +21 -0
- package/README.md +69 -0
- package/lib/did-cache-kv.d.ts +18 -0
- package/lib/did-cache-kv.js +26 -0
- package/lib/did-resolver/did-cache-memory.d.ts +7 -0
- package/lib/did-resolver/did-cache-memory.js +10 -0
- package/lib/did-resolver/did-cache.d.ts +14 -0
- package/lib/did-resolver/did-cache.js +10 -0
- package/lib/did-resolver/did-method.d.ts +11 -0
- package/lib/did-resolver/did-method.js +1 -0
- package/lib/did-resolver/did-resolver-base.d.ts +9 -0
- package/lib/did-resolver/did-resolver-base.js +36 -0
- package/lib/did-resolver/did-resolver-common.d.ts +8 -0
- package/lib/did-resolver/did-resolver-common.js +11 -0
- package/lib/did-resolver/did-resolver.d.ts +6 -0
- package/lib/did-resolver/did-resolver.js +1 -0
- package/lib/did-resolver/index.d.ts +6 -0
- package/lib/did-resolver/index.js +7 -0
- package/lib/did-resolver/methods/plc.d.ts +43 -0
- package/lib/did-resolver/methods/plc.js +22 -0
- package/lib/did-resolver/methods/web.d.ts +43 -0
- package/lib/did-resolver/methods/web.js +42 -0
- package/lib/did-resolver/methods.d.ts +2 -0
- package/lib/did-resolver/methods.js +2 -0
- package/lib/did-resolver/util.d.ts +3 -0
- package/lib/did-resolver/util.js +1 -0
- package/lib/dpop-store.d.ts +21 -0
- package/lib/dpop-store.js +25 -0
- package/lib/handle-cache-kv.d.ts +17 -0
- package/lib/handle-cache-kv.js +31 -0
- package/lib/handle-resolver/atproto-doh-handle-resolver.d.ts +8 -0
- package/lib/handle-resolver/atproto-doh-handle-resolver.js +94 -0
- package/lib/handle-resolver/atproto-handle-resolver.d.ts +21 -0
- package/lib/handle-resolver/atproto-handle-resolver.js +46 -0
- package/lib/handle-resolver/cached-handle-resolver.d.ts +12 -0
- package/lib/handle-resolver/cached-handle-resolver.js +17 -0
- package/lib/handle-resolver/handle-resolver-error.d.ts +3 -0
- package/lib/handle-resolver/handle-resolver-error.js +6 -0
- package/lib/handle-resolver/index.d.ts +6 -0
- package/lib/handle-resolver/index.js +8 -0
- package/lib/handle-resolver/internal-resolvers/dns-handle-resolver.d.ts +11 -0
- package/lib/handle-resolver/internal-resolvers/dns-handle-resolver.js +28 -0
- package/lib/handle-resolver/internal-resolvers/well-known-handler-resolver.d.ts +17 -0
- package/lib/handle-resolver/internal-resolvers/well-known-handler-resolver.js +28 -0
- package/lib/handle-resolver/types.d.ts +25 -0
- package/lib/handle-resolver/types.js +10 -0
- package/lib/handle-resolver/xrpc-handle-resolver.d.ts +31 -0
- package/lib/handle-resolver/xrpc-handle-resolver.js +45 -0
- package/lib/handle-resolver.d.ts +20 -0
- package/lib/handle-resolver.js +19 -0
- package/lib/identity-resolver/atproto-identity-resolver.d.ts +20 -0
- package/lib/identity-resolver/atproto-identity-resolver.js +72 -0
- package/lib/identity-resolver/constants.d.ts +1 -0
- package/lib/identity-resolver/constants.js +1 -0
- package/lib/identity-resolver/identity-resolver-error.d.ts +3 -0
- package/lib/identity-resolver/identity-resolver-error.js +6 -0
- package/lib/identity-resolver/identity-resolver.d.ts +19 -0
- package/lib/identity-resolver/identity-resolver.js +1 -0
- package/lib/identity-resolver/index.d.ts +5 -0
- package/lib/identity-resolver/index.js +5 -0
- package/lib/identity-resolver/util.d.ts +12 -0
- package/lib/identity-resolver/util.js +35 -0
- package/lib/index.d.ts +7 -0
- package/lib/index.js +6 -0
- package/lib/oauth-client/atproto-token-response.d.ts +100 -0
- package/lib/oauth-client/atproto-token-response.js +15 -0
- package/lib/oauth-client/constants.d.ts +4 -0
- package/lib/oauth-client/constants.js +4 -0
- package/lib/oauth-client/errors/auth-method-unsatisfiable-error.d.ts +2 -0
- package/lib/oauth-client/errors/auth-method-unsatisfiable-error.js +2 -0
- package/lib/oauth-client/errors/token-invalid-error.d.ts +6 -0
- package/lib/oauth-client/errors/token-invalid-error.js +6 -0
- package/lib/oauth-client/errors/token-refresh-error.d.ts +6 -0
- package/lib/oauth-client/errors/token-refresh-error.js +6 -0
- package/lib/oauth-client/errors/token-revoked-error.d.ts +6 -0
- package/lib/oauth-client/errors/token-revoked-error.js +6 -0
- package/lib/oauth-client/fetch-dpop.d.ts +19 -0
- package/lib/oauth-client/fetch-dpop.js +176 -0
- package/lib/oauth-client/identity-resolver.d.ts +15 -0
- package/lib/oauth-client/identity-resolver.js +33 -0
- package/lib/oauth-client/index.d.ts +17 -0
- package/lib/oauth-client/index.js +17 -0
- package/lib/oauth-client/lock.d.ts +2 -0
- package/lib/oauth-client/lock.js +28 -0
- package/lib/oauth-client/oauth-authorization-server-metadata-resolver.d.ts +18 -0
- package/lib/oauth-client/oauth-authorization-server-metadata-resolver.js +53 -0
- package/lib/oauth-client/oauth-callback-error.d.ts +6 -0
- package/lib/oauth-client/oauth-callback-error.js +13 -0
- package/lib/oauth-client/oauth-client-auth.d.ts +22 -0
- package/lib/oauth-client/oauth-client-auth.js +127 -0
- package/lib/oauth-client/oauth-client.d.ts +311 -0
- package/lib/oauth-client/oauth-client.js +276 -0
- package/lib/oauth-client/oauth-protected-resource-metadata-resolver.d.ts +18 -0
- package/lib/oauth-client/oauth-protected-resource-metadata-resolver.js +49 -0
- package/lib/oauth-client/oauth-resolver-error.d.ts +6 -0
- package/lib/oauth-client/oauth-resolver-error.js +18 -0
- package/lib/oauth-client/oauth-resolver.d.ts +71 -0
- package/lib/oauth-client/oauth-resolver.js +117 -0
- package/lib/oauth-client/oauth-response-error.d.ts +10 -0
- package/lib/oauth-client/oauth-response-error.js +22 -0
- package/lib/oauth-client/oauth-server-agent.d.ts +54 -0
- package/lib/oauth-client/oauth-server-agent.js +250 -0
- package/lib/oauth-client/oauth-server-factory.d.ts +32 -0
- package/lib/oauth-client/oauth-server-factory.js +37 -0
- package/lib/oauth-client/oauth-session.d.ts +33 -0
- package/lib/oauth-client/oauth-session.js +122 -0
- package/lib/oauth-client/runtime-implementation.d.ts +16 -0
- package/lib/oauth-client/runtime-implementation.js +1 -0
- package/lib/oauth-client/runtime.d.ts +25 -0
- package/lib/oauth-client/runtime.js +99 -0
- package/lib/oauth-client/session-getter.d.ts +54 -0
- package/lib/oauth-client/session-getter.js +260 -0
- package/lib/oauth-client/state-store.d.ts +12 -0
- package/lib/oauth-client/state-store.js +1 -0
- package/lib/oauth-client/types.d.ts +1365 -0
- package/lib/oauth-client/types.js +8 -0
- package/lib/oauth-client/util.d.ts +25 -0
- package/lib/oauth-client/util.js +139 -0
- package/lib/oauth-client/validate-client-metadata.d.ts +4 -0
- package/lib/oauth-client/validate-client-metadata.js +68 -0
- package/lib/oauth-client.d.ts +27 -0
- package/lib/oauth-client.js +30 -0
- package/lib/resolve-txt-factory.d.ts +3 -0
- package/lib/resolve-txt-factory.js +80 -0
- package/lib/session-store-kv.d.ts +9 -0
- package/lib/session-store-kv.js +20 -0
- package/lib/state-store-kv.d.ts +9 -0
- package/lib/state-store-kv.js +20 -0
- package/lib/util.d.ts +18 -0
- package/lib/util.js +5 -0
- package/package.json +58 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { oauthClientIdDiscoverableSchema, oauthClientIdLoopbackSchema, oauthClientMetadataSchema, } from "@atproto/oauth-types";
|
|
3
|
+
export const clientMetadataSchema = oauthClientMetadataSchema.extend({
|
|
4
|
+
client_id: z.union([
|
|
5
|
+
oauthClientIdDiscoverableSchema,
|
|
6
|
+
oauthClientIdLoopbackSchema,
|
|
7
|
+
]),
|
|
8
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type Awaitable<T> = T | PromiseLike<T>;
|
|
2
|
+
export type Simplify<T> = {
|
|
3
|
+
[K in keyof T]: T[K];
|
|
4
|
+
} & NonNullable<unknown>;
|
|
5
|
+
export declare const ifString: <V>(v: V) => (V & string) | undefined;
|
|
6
|
+
/**
|
|
7
|
+
* @todo (?) move to common package
|
|
8
|
+
*/
|
|
9
|
+
export declare const timeoutSignal: (timeout: number, options?: {
|
|
10
|
+
signal?: AbortSignal;
|
|
11
|
+
}) => AbortSignal & Disposable;
|
|
12
|
+
export declare function contentMime(headers: Headers): string | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* Ponyfill for `CustomEvent` constructor.
|
|
15
|
+
*/
|
|
16
|
+
export declare const CustomEvent: typeof globalThis.CustomEvent;
|
|
17
|
+
export declare class CustomEventTarget<EventDetailMap extends Record<string, unknown>> {
|
|
18
|
+
readonly eventTarget: EventTarget;
|
|
19
|
+
addEventListener<T extends Extract<keyof EventDetailMap, string>>(type: T, callback: (event: CustomEvent<EventDetailMap[T]>) => void, options?: AddEventListenerOptions | boolean): void;
|
|
20
|
+
removeEventListener<T extends Extract<keyof EventDetailMap, string>>(type: T, callback: (event: CustomEvent<EventDetailMap[T]>) => void, options?: EventListenerOptions | boolean): void;
|
|
21
|
+
dispatchCustomEvent<T extends Extract<keyof EventDetailMap, string>>(type: T, detail: EventDetailMap[T], init?: EventInit): boolean;
|
|
22
|
+
}
|
|
23
|
+
export type SpaceSeparatedValue<Value extends string> = `${Value}` | `${Value} ${string}` | `${string} ${Value}` | `${string} ${Value} ${string}`;
|
|
24
|
+
export declare const includesSpaceSeparatedValue: <Value extends string>(input: string, value: Value) => input is SpaceSeparatedValue<Value>;
|
|
25
|
+
export declare function combineSignals(signals: readonly (AbortSignal | undefined)[]): AbortController & Disposable;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
2
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
3
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
4
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
5
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
6
|
+
};
|
|
7
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
8
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
9
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
10
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
11
|
+
};
|
|
12
|
+
// @ts-expect-error
|
|
13
|
+
Symbol.dispose ?? (Symbol.dispose = Symbol("@@dispose"));
|
|
14
|
+
export const ifString = (v) => (typeof v === "string" ? v : undefined);
|
|
15
|
+
/**
|
|
16
|
+
* @todo (?) move to common package
|
|
17
|
+
*/
|
|
18
|
+
export const timeoutSignal = (timeout, options) => {
|
|
19
|
+
if (!Number.isInteger(timeout) || timeout < 0) {
|
|
20
|
+
throw new TypeError("Expected a positive integer");
|
|
21
|
+
}
|
|
22
|
+
options?.signal?.throwIfAborted();
|
|
23
|
+
const controller = new AbortController();
|
|
24
|
+
const { signal } = controller;
|
|
25
|
+
options?.signal?.addEventListener("abort", (reason) => controller.abort(reason), { once: true, signal });
|
|
26
|
+
const timeoutId = setTimeout((err) => controller.abort(err), timeout,
|
|
27
|
+
// create Error here to keep original stack trace
|
|
28
|
+
new Error("Timeout"));
|
|
29
|
+
timeoutId?.unref?.(); // NodeJS only
|
|
30
|
+
signal.addEventListener("abort", () => clearTimeout(timeoutId), {
|
|
31
|
+
once: true,
|
|
32
|
+
signal,
|
|
33
|
+
});
|
|
34
|
+
Object.defineProperty(signal, Symbol.dispose, {
|
|
35
|
+
value: () => controller.abort(),
|
|
36
|
+
});
|
|
37
|
+
return signal;
|
|
38
|
+
};
|
|
39
|
+
export function contentMime(headers) {
|
|
40
|
+
return headers.get("content-type")?.split(";")[0].trim();
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Ponyfill for `CustomEvent` constructor.
|
|
44
|
+
*/
|
|
45
|
+
export const CustomEvent = globalThis.CustomEvent ??
|
|
46
|
+
(() => {
|
|
47
|
+
var _CustomEvent_detail;
|
|
48
|
+
class CustomEvent extends Event {
|
|
49
|
+
constructor(type, options) {
|
|
50
|
+
if (!arguments.length)
|
|
51
|
+
throw new TypeError("type argument is required");
|
|
52
|
+
super(type, options);
|
|
53
|
+
_CustomEvent_detail.set(this, void 0);
|
|
54
|
+
__classPrivateFieldSet(this, _CustomEvent_detail, options?.detail ?? null, "f");
|
|
55
|
+
}
|
|
56
|
+
get detail() {
|
|
57
|
+
return __classPrivateFieldGet(this, _CustomEvent_detail, "f");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
_CustomEvent_detail = new WeakMap();
|
|
61
|
+
Object.defineProperties(CustomEvent.prototype, {
|
|
62
|
+
[Symbol.toStringTag]: {
|
|
63
|
+
writable: false,
|
|
64
|
+
enumerable: false,
|
|
65
|
+
configurable: true,
|
|
66
|
+
value: "CustomEvent",
|
|
67
|
+
},
|
|
68
|
+
detail: {
|
|
69
|
+
enumerable: true,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
return CustomEvent;
|
|
73
|
+
})();
|
|
74
|
+
export class CustomEventTarget {
|
|
75
|
+
constructor() {
|
|
76
|
+
this.eventTarget = new EventTarget();
|
|
77
|
+
}
|
|
78
|
+
addEventListener(type, callback, options) {
|
|
79
|
+
this.eventTarget.addEventListener(type, callback, options);
|
|
80
|
+
}
|
|
81
|
+
removeEventListener(type, callback, options) {
|
|
82
|
+
this.eventTarget.removeEventListener(type, callback, options);
|
|
83
|
+
}
|
|
84
|
+
dispatchCustomEvent(type, detail, init) {
|
|
85
|
+
return this.eventTarget.dispatchEvent(new CustomEvent(type, { ...init, detail }));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export const includesSpaceSeparatedValue = (input, value) => {
|
|
89
|
+
if (value.length === 0)
|
|
90
|
+
throw new TypeError("Value cannot be empty");
|
|
91
|
+
if (value.includes(" "))
|
|
92
|
+
throw new TypeError("Value cannot contain spaces");
|
|
93
|
+
// Optimized version of:
|
|
94
|
+
// return input.split(' ').includes(value)
|
|
95
|
+
const inputLength = input.length;
|
|
96
|
+
const valueLength = value.length;
|
|
97
|
+
if (inputLength < valueLength)
|
|
98
|
+
return false;
|
|
99
|
+
let idx = input.indexOf(value);
|
|
100
|
+
let idxEnd;
|
|
101
|
+
while (idx !== -1) {
|
|
102
|
+
idxEnd = idx + valueLength;
|
|
103
|
+
if (
|
|
104
|
+
// at beginning or preceded by space
|
|
105
|
+
(idx === 0 || input[idx - 1] === " ") &&
|
|
106
|
+
// at end or followed by space
|
|
107
|
+
(idxEnd === inputLength || input[idxEnd] === " ")) {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
idx = input.indexOf(value, idxEnd + 1);
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
};
|
|
114
|
+
export function combineSignals(signals) {
|
|
115
|
+
const controller = new AbortController();
|
|
116
|
+
const onAbort = function (_event) {
|
|
117
|
+
const reason = new Error("This operation was aborted", {
|
|
118
|
+
cause: this.reason,
|
|
119
|
+
});
|
|
120
|
+
controller.abort(reason);
|
|
121
|
+
};
|
|
122
|
+
for (const sig of signals) {
|
|
123
|
+
if (!sig)
|
|
124
|
+
continue;
|
|
125
|
+
if (sig.aborted) {
|
|
126
|
+
// Remove "abort" listener that was added to sig in previous iterations
|
|
127
|
+
controller.abort();
|
|
128
|
+
throw new Error("One of the signals is already aborted", {
|
|
129
|
+
cause: sig.reason,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
sig.addEventListener("abort", onAbort, { signal: controller.signal });
|
|
133
|
+
}
|
|
134
|
+
controller[Symbol.dispose] = () => {
|
|
135
|
+
const reason = new Error("AbortController was disposed");
|
|
136
|
+
controller.abort(reason);
|
|
137
|
+
};
|
|
138
|
+
return controller;
|
|
139
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { assertOAuthDiscoverableClientId, assertOAuthLoopbackClientId, } from "@atproto/oauth-types";
|
|
2
|
+
import { FALLBACK_ALG } from "./constants.js";
|
|
3
|
+
import { clientMetadataSchema } from "./types.js";
|
|
4
|
+
export function validateClientMetadata(input, keyset) {
|
|
5
|
+
// Allow to pass a keyset and omit the jwks/jwks_uri properties
|
|
6
|
+
if (!input.jwks && !input.jwks_uri && keyset?.size) {
|
|
7
|
+
input = { ...input, jwks: keyset.toJSON() };
|
|
8
|
+
}
|
|
9
|
+
const metadata = clientMetadataSchema.parse(input);
|
|
10
|
+
// Validate client ID
|
|
11
|
+
if (metadata.client_id.startsWith("http:")) {
|
|
12
|
+
assertOAuthLoopbackClientId(metadata.client_id);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
assertOAuthDiscoverableClientId(metadata.client_id);
|
|
16
|
+
}
|
|
17
|
+
const scopes = metadata.scope?.split(" ");
|
|
18
|
+
if (!scopes?.includes("atproto")) {
|
|
19
|
+
throw new TypeError(`Client metadata must include the "atproto" scope`);
|
|
20
|
+
}
|
|
21
|
+
if (!metadata.response_types.includes("code")) {
|
|
22
|
+
throw new TypeError(`"response_types" must include "code"`);
|
|
23
|
+
}
|
|
24
|
+
if (!metadata.grant_types.includes("authorization_code")) {
|
|
25
|
+
throw new TypeError(`"grant_types" must include "authorization_code"`);
|
|
26
|
+
}
|
|
27
|
+
const method = metadata.token_endpoint_auth_method;
|
|
28
|
+
const methodAlg = metadata.token_endpoint_auth_signing_alg;
|
|
29
|
+
switch (method) {
|
|
30
|
+
case "none":
|
|
31
|
+
if (methodAlg) {
|
|
32
|
+
throw new TypeError(`"token_endpoint_auth_signing_alg" must not be provided when "token_endpoint_auth_method" is "${method}"`);
|
|
33
|
+
}
|
|
34
|
+
break;
|
|
35
|
+
case "private_key_jwt": {
|
|
36
|
+
if (!methodAlg) {
|
|
37
|
+
throw new TypeError(`"token_endpoint_auth_signing_alg" must be provided when "token_endpoint_auth_method" is "${method}"`);
|
|
38
|
+
}
|
|
39
|
+
const signingKeys = keyset
|
|
40
|
+
? Array.from(keyset.list({ use: "sig" })).filter((key) => key.isPrivate && key.kid)
|
|
41
|
+
: null;
|
|
42
|
+
if (!signingKeys?.some((key) => key.algorithms.includes(FALLBACK_ALG))) {
|
|
43
|
+
throw new TypeError(`Client authentication method "${method}" requires at least one "${FALLBACK_ALG}" signing key with a "kid" property`);
|
|
44
|
+
}
|
|
45
|
+
if (metadata.jwks) {
|
|
46
|
+
// Ensure that all the signing keys that could end-up being used are
|
|
47
|
+
// advertised in the JWKS.
|
|
48
|
+
for (const key of signingKeys) {
|
|
49
|
+
if (!metadata.jwks.keys.some((k) => k.kid === key.kid)) {
|
|
50
|
+
throw new TypeError(`Key with kid "${key.kid}" not found in jwks`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else if (metadata.jwks_uri) {
|
|
55
|
+
// @NOTE we only ensure that all the signing keys are referenced in JWKS
|
|
56
|
+
// when it is available (see previous "if") as we don't want to download
|
|
57
|
+
// that file here (for efficiency reasons).
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
throw new TypeError(`Client authentication method "${method}" requires a JWKS`);
|
|
61
|
+
}
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
default:
|
|
65
|
+
throw new TypeError(`Unsupported "token_endpoint_auth_method" value: ${method}`);
|
|
66
|
+
}
|
|
67
|
+
return metadata;
|
|
68
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { HandleResolver, OAuthClient, OAuthClientFetchMetadataOptions, OAuthClientOptions, RuntimeImplementation, RuntimeLock } from "#oauth-client";
|
|
2
|
+
import { OAuthResponseMode } from "@atproto/oauth-types";
|
|
3
|
+
import { AtprotoHandleResolverWorkersOptions } from "./handle-resolver.js";
|
|
4
|
+
import { WorkersSavedSessionStore, WorkersSavedStateStore } from "./dpop-store.js";
|
|
5
|
+
import { Override } from "./util.js";
|
|
6
|
+
export type WorkersOAuthClientOptions = Override<OAuthClientOptions, {
|
|
7
|
+
responseMode?: Exclude<OAuthResponseMode, "fragment">;
|
|
8
|
+
stateStore: WorkersSavedStateStore;
|
|
9
|
+
sessionStore: WorkersSavedSessionStore;
|
|
10
|
+
/**
|
|
11
|
+
* Used to build a {@link WorkersOAuthClientOptions.handleResolver} if none is
|
|
12
|
+
* provided.
|
|
13
|
+
*/
|
|
14
|
+
fallbackNameservers?: AtprotoHandleResolverWorkersOptions["fallbackNameservers"];
|
|
15
|
+
handleResolver?: HandleResolver | string | URL;
|
|
16
|
+
/**
|
|
17
|
+
* Used to build a {@link WorkersOAuthClientOptions.runtimeImplementation} if
|
|
18
|
+
* none is provided. Pass in `requestLocalLock` from `@atproto/oauth-client`
|
|
19
|
+
* to mute warning.
|
|
20
|
+
*/
|
|
21
|
+
requestLock?: RuntimeLock;
|
|
22
|
+
runtimeImplementation?: RuntimeImplementation;
|
|
23
|
+
}>;
|
|
24
|
+
export type WorkersOAuthClientFromMetadataOptions = OAuthClientFetchMetadataOptions & Omit<WorkersOAuthClientOptions, "clientMetadata">;
|
|
25
|
+
export declare class WorkersOAuthClient extends OAuthClient {
|
|
26
|
+
constructor({ requestLock, fallbackNameservers, fetch, responseMode, stateStore, sessionStore, handleResolver, runtimeImplementation, ...options }: WorkersOAuthClientOptions);
|
|
27
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { JoseKey } from "@atproto/jwk-jose";
|
|
3
|
+
import { OAuthClient, } from "#oauth-client";
|
|
4
|
+
import { AtprotoHandleResolverWorkers, } from "./handle-resolver.js";
|
|
5
|
+
import { toDpopKeyStore, } from "./dpop-store.js";
|
|
6
|
+
export class WorkersOAuthClient extends OAuthClient {
|
|
7
|
+
constructor({ requestLock = undefined, fallbackNameservers = undefined, fetch, responseMode = "query", stateStore, sessionStore, handleResolver = new AtprotoHandleResolverWorkers({
|
|
8
|
+
fetch,
|
|
9
|
+
fallbackNameservers,
|
|
10
|
+
}), runtimeImplementation = {
|
|
11
|
+
requestLock,
|
|
12
|
+
createKey: (algs) => JoseKey.generate(algs),
|
|
13
|
+
getRandomValues: randomBytes,
|
|
14
|
+
digest: (bytes, algorithm) => createHash(algorithm.name).update(bytes).digest(),
|
|
15
|
+
}, ...options }) {
|
|
16
|
+
if (!runtimeImplementation.requestLock) {
|
|
17
|
+
// Ok if only one instance of the client is running at a time.
|
|
18
|
+
console.warn("No lock mechanism provided. Credentials might get revoked.");
|
|
19
|
+
}
|
|
20
|
+
super({
|
|
21
|
+
...options,
|
|
22
|
+
fetch,
|
|
23
|
+
responseMode,
|
|
24
|
+
handleResolver,
|
|
25
|
+
runtimeImplementation,
|
|
26
|
+
stateStore: toDpopKeyStore(stateStore),
|
|
27
|
+
sessionStore: toDpopKeyStore(sessionStore),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Resolver, lookup, resolveTxt } from "node:dns/promises";
|
|
2
|
+
import { isIP } from "node:net";
|
|
3
|
+
import { isSystemError } from "./util.js";
|
|
4
|
+
export const resolveTxtDefault = (hostname) => {
|
|
5
|
+
return resolveTxt(hostname).then(groupChunks, handleError);
|
|
6
|
+
};
|
|
7
|
+
export function resolveTxtFactory(nameservers) {
|
|
8
|
+
// Optimization
|
|
9
|
+
if (!nameservers.length)
|
|
10
|
+
return async () => null;
|
|
11
|
+
// Build the resolver asynchronously (will be awaited on every use)
|
|
12
|
+
const resolverPromise = Promise.all(nameservers.map((nameserver) => {
|
|
13
|
+
const [domain, port = null] = nameserver.split(":", 2);
|
|
14
|
+
if (port !== null && !/^\d+$/.test(port)) {
|
|
15
|
+
throw new TypeError(`Invalid name server "${nameserver}"`);
|
|
16
|
+
}
|
|
17
|
+
return isIP(domain) === 4 || isBracedIPv6(domain)
|
|
18
|
+
? [nameserver] // No need to lookup
|
|
19
|
+
: lookup(domain, { all: true }).then((r) => r.map((a) => appendPort(a.address, port)),
|
|
20
|
+
// Let's just ignore failed nameservers resolution
|
|
21
|
+
(_err) => []);
|
|
22
|
+
})).then((results) => {
|
|
23
|
+
const backupIps = results.flat(1);
|
|
24
|
+
// No resolver if no valid IP
|
|
25
|
+
if (!backupIps.length)
|
|
26
|
+
return null;
|
|
27
|
+
const resolver = new Resolver();
|
|
28
|
+
resolver.setServers(backupIps);
|
|
29
|
+
return resolver;
|
|
30
|
+
});
|
|
31
|
+
// Avoid uncaught promise rejection
|
|
32
|
+
void resolverPromise.catch(() => {
|
|
33
|
+
// Should never happen though...
|
|
34
|
+
});
|
|
35
|
+
return async (hostname) => {
|
|
36
|
+
const resolver = await resolverPromise;
|
|
37
|
+
return resolver
|
|
38
|
+
? resolver.resolveTxt(hostname).then(groupChunks, handleError)
|
|
39
|
+
: null;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function isBracedIPv6(address) {
|
|
43
|
+
return (address.startsWith("[") &&
|
|
44
|
+
address.endsWith("]") &&
|
|
45
|
+
isIP(address.slice(1, -1)) === 6);
|
|
46
|
+
}
|
|
47
|
+
function groupChunks(results) {
|
|
48
|
+
return results.map((chunks) => chunks.join(""));
|
|
49
|
+
}
|
|
50
|
+
function handleError(err) {
|
|
51
|
+
// Invalid argument type (e.g. hostname is a number)
|
|
52
|
+
if (err instanceof TypeError)
|
|
53
|
+
throw err;
|
|
54
|
+
// If the hostname does not resolve, return null
|
|
55
|
+
if (isSystemError(err)) {
|
|
56
|
+
if (err["code"] === "ENOTFOUND")
|
|
57
|
+
return null;
|
|
58
|
+
// Hostname is not a valid domain name
|
|
59
|
+
if (err["code"] === "EBADNAME")
|
|
60
|
+
throw err;
|
|
61
|
+
// DNS server unreachable
|
|
62
|
+
// if (err['code'] === 'ETIMEOUT') throw err
|
|
63
|
+
}
|
|
64
|
+
// Historically, errors were not thrown here. A "null" value indicates to the
|
|
65
|
+
// AtprotoHandleResolver that it should try the fallback resolver.
|
|
66
|
+
// @TODO We might want to re-visit this to only apply when an unexpected error
|
|
67
|
+
// occurs (by throwing here). For now, let's keep the same behavior as before.
|
|
68
|
+
// throw err
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
function appendPort(address, port) {
|
|
72
|
+
switch (isIP(address)) {
|
|
73
|
+
case 4:
|
|
74
|
+
return port ? `${address}:${port}` : address;
|
|
75
|
+
case 6:
|
|
76
|
+
return port ? `[${address}]:${port}` : `[${address}]`;
|
|
77
|
+
default:
|
|
78
|
+
throw new TypeError(`Invalid IP address "${address}"`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { WorkersSavedSession, WorkersSavedSessionStore } from "./dpop-store.js";
|
|
2
|
+
import type { KVNamespace } from "./util.js";
|
|
3
|
+
export declare class SessionStoreKV implements WorkersSavedSessionStore {
|
|
4
|
+
namespace: KVNamespace;
|
|
5
|
+
constructor(namespace: KVNamespace);
|
|
6
|
+
set(sub: string, session: WorkersSavedSession): Promise<void>;
|
|
7
|
+
get(sub: string): Promise<WorkersSavedSession | undefined>;
|
|
8
|
+
del(sub: string): Promise<void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export class SessionStoreKV {
|
|
2
|
+
constructor(namespace) {
|
|
3
|
+
this.namespace = namespace;
|
|
4
|
+
}
|
|
5
|
+
async set(sub, session) {
|
|
6
|
+
await this.namespace.put(sub, JSON.stringify(session));
|
|
7
|
+
}
|
|
8
|
+
async get(sub) {
|
|
9
|
+
const value = await this.namespace.get(sub);
|
|
10
|
+
if (value === null) {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
return JSON.parse(value);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
async del(sub) {
|
|
18
|
+
await this.namespace.delete(sub);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { WorkersSavedState, WorkersSavedStateStore } from "./dpop-store.js";
|
|
2
|
+
import type { KVNamespace } from "./util.js";
|
|
3
|
+
export declare class StateStoreKV implements WorkersSavedStateStore {
|
|
4
|
+
namespace: KVNamespace;
|
|
5
|
+
constructor(namespace: KVNamespace);
|
|
6
|
+
set(key: string, internalState: WorkersSavedState): Promise<void>;
|
|
7
|
+
get(key: string): Promise<WorkersSavedState | undefined>;
|
|
8
|
+
del(key: string): Promise<void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export class StateStoreKV {
|
|
2
|
+
constructor(namespace) {
|
|
3
|
+
this.namespace = namespace;
|
|
4
|
+
}
|
|
5
|
+
async set(key, internalState) {
|
|
6
|
+
await this.namespace.put(key, JSON.stringify(internalState));
|
|
7
|
+
}
|
|
8
|
+
async get(key) {
|
|
9
|
+
const value = await this.namespace.get(key);
|
|
10
|
+
if (value === null) {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
return JSON.parse(value);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
async del(key) {
|
|
18
|
+
await this.namespace.delete(key);
|
|
19
|
+
}
|
|
20
|
+
}
|
package/lib/util.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type Simplify<T> = {
|
|
2
|
+
[K in keyof T]: T[K];
|
|
3
|
+
} & {};
|
|
4
|
+
export type Override<T, V> = Simplify<V & Omit<T, keyof V>>;
|
|
5
|
+
export interface SystemError extends Error {
|
|
6
|
+
code: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function isSystemError(error: any): error is SystemError;
|
|
9
|
+
export interface KVNamespacePutOptions {
|
|
10
|
+
expiration?: number;
|
|
11
|
+
expirationTtl?: number;
|
|
12
|
+
metadata?: any | null;
|
|
13
|
+
}
|
|
14
|
+
export interface KVNamespace {
|
|
15
|
+
get(key: string): Promise<string | null>;
|
|
16
|
+
put(key: string, value: string, options?: KVNamespacePutOptions): Promise<void>;
|
|
17
|
+
delete(key: string): Promise<void>;
|
|
18
|
+
}
|
package/lib/util.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bskyprism/atproto-oauth-client-cloudflare-workers",
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"files": [
|
|
6
|
+
"lib"
|
|
7
|
+
],
|
|
8
|
+
"main": "lib/index.js",
|
|
9
|
+
"types": "./lib/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./lib/index.js",
|
|
12
|
+
"./did-resolver": "./lib/did-resolver/index.js",
|
|
13
|
+
"./handle-resolver": "./lib/handle-resolver/index.js",
|
|
14
|
+
"./identity-resolver": "./lib/identity-resolver/index.js",
|
|
15
|
+
"./oauth-client": "./lib/oauth-client/index.js"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"dev": "tsc --build tsconfig.json --watch",
|
|
19
|
+
"build": "tsc --build tsconfig.json",
|
|
20
|
+
"clean": "tsc --build tsconfig.json --clean",
|
|
21
|
+
"prepublishOnly": "npm run build"
|
|
22
|
+
},
|
|
23
|
+
"imports": {
|
|
24
|
+
"#did-resolver": "./lib/did-resolver/index.js",
|
|
25
|
+
"#handle-resolver": "./lib/handle-resolver/index.js",
|
|
26
|
+
"#identity-resolver": "./lib/identity-resolver/index.js",
|
|
27
|
+
"#oauth-client": "./lib/oauth-client/index.js"
|
|
28
|
+
},
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/nDimensional/atproto-oauth-client-cloudflare-workers.git"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"atproto"
|
|
35
|
+
],
|
|
36
|
+
"author": "Joel Gustafson",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/nDimensional/atproto-oauth-client-cloudflare-workers/issues"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/nDimensional/atproto-oauth-client-cloudflare-workers#readme",
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^24.5.2",
|
|
44
|
+
"prettier": "^3.6.2",
|
|
45
|
+
"typescript": "^5.9.2"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@atproto-labs/fetch": "^0.2.3",
|
|
49
|
+
"@atproto-labs/pipe": "^0.1.1",
|
|
50
|
+
"@atproto-labs/simple-store": "^0.3.0",
|
|
51
|
+
"@atproto-labs/simple-store-memory": "^0.1.4",
|
|
52
|
+
"@atproto/did": "^0.2.0",
|
|
53
|
+
"@atproto/jwk-jose": "^0.1.10",
|
|
54
|
+
"@atproto/oauth-types": "^0.4.1",
|
|
55
|
+
"multiformats": "^13.4.1",
|
|
56
|
+
"zod": "^3.25.76"
|
|
57
|
+
}
|
|
58
|
+
}
|