@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.
Files changed (131) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +69 -0
  3. package/lib/did-cache-kv.d.ts +18 -0
  4. package/lib/did-cache-kv.js +26 -0
  5. package/lib/did-resolver/did-cache-memory.d.ts +7 -0
  6. package/lib/did-resolver/did-cache-memory.js +10 -0
  7. package/lib/did-resolver/did-cache.d.ts +14 -0
  8. package/lib/did-resolver/did-cache.js +10 -0
  9. package/lib/did-resolver/did-method.d.ts +11 -0
  10. package/lib/did-resolver/did-method.js +1 -0
  11. package/lib/did-resolver/did-resolver-base.d.ts +9 -0
  12. package/lib/did-resolver/did-resolver-base.js +36 -0
  13. package/lib/did-resolver/did-resolver-common.d.ts +8 -0
  14. package/lib/did-resolver/did-resolver-common.js +11 -0
  15. package/lib/did-resolver/did-resolver.d.ts +6 -0
  16. package/lib/did-resolver/did-resolver.js +1 -0
  17. package/lib/did-resolver/index.d.ts +6 -0
  18. package/lib/did-resolver/index.js +7 -0
  19. package/lib/did-resolver/methods/plc.d.ts +43 -0
  20. package/lib/did-resolver/methods/plc.js +22 -0
  21. package/lib/did-resolver/methods/web.d.ts +43 -0
  22. package/lib/did-resolver/methods/web.js +42 -0
  23. package/lib/did-resolver/methods.d.ts +2 -0
  24. package/lib/did-resolver/methods.js +2 -0
  25. package/lib/did-resolver/util.d.ts +3 -0
  26. package/lib/did-resolver/util.js +1 -0
  27. package/lib/dpop-store.d.ts +21 -0
  28. package/lib/dpop-store.js +25 -0
  29. package/lib/handle-cache-kv.d.ts +17 -0
  30. package/lib/handle-cache-kv.js +31 -0
  31. package/lib/handle-resolver/atproto-doh-handle-resolver.d.ts +8 -0
  32. package/lib/handle-resolver/atproto-doh-handle-resolver.js +94 -0
  33. package/lib/handle-resolver/atproto-handle-resolver.d.ts +21 -0
  34. package/lib/handle-resolver/atproto-handle-resolver.js +46 -0
  35. package/lib/handle-resolver/cached-handle-resolver.d.ts +12 -0
  36. package/lib/handle-resolver/cached-handle-resolver.js +17 -0
  37. package/lib/handle-resolver/handle-resolver-error.d.ts +3 -0
  38. package/lib/handle-resolver/handle-resolver-error.js +6 -0
  39. package/lib/handle-resolver/index.d.ts +6 -0
  40. package/lib/handle-resolver/index.js +8 -0
  41. package/lib/handle-resolver/internal-resolvers/dns-handle-resolver.d.ts +11 -0
  42. package/lib/handle-resolver/internal-resolvers/dns-handle-resolver.js +28 -0
  43. package/lib/handle-resolver/internal-resolvers/well-known-handler-resolver.d.ts +17 -0
  44. package/lib/handle-resolver/internal-resolvers/well-known-handler-resolver.js +28 -0
  45. package/lib/handle-resolver/types.d.ts +25 -0
  46. package/lib/handle-resolver/types.js +10 -0
  47. package/lib/handle-resolver/xrpc-handle-resolver.d.ts +31 -0
  48. package/lib/handle-resolver/xrpc-handle-resolver.js +45 -0
  49. package/lib/handle-resolver.d.ts +20 -0
  50. package/lib/handle-resolver.js +19 -0
  51. package/lib/identity-resolver/atproto-identity-resolver.d.ts +20 -0
  52. package/lib/identity-resolver/atproto-identity-resolver.js +72 -0
  53. package/lib/identity-resolver/constants.d.ts +1 -0
  54. package/lib/identity-resolver/constants.js +1 -0
  55. package/lib/identity-resolver/identity-resolver-error.d.ts +3 -0
  56. package/lib/identity-resolver/identity-resolver-error.js +6 -0
  57. package/lib/identity-resolver/identity-resolver.d.ts +19 -0
  58. package/lib/identity-resolver/identity-resolver.js +1 -0
  59. package/lib/identity-resolver/index.d.ts +5 -0
  60. package/lib/identity-resolver/index.js +5 -0
  61. package/lib/identity-resolver/util.d.ts +12 -0
  62. package/lib/identity-resolver/util.js +35 -0
  63. package/lib/index.d.ts +7 -0
  64. package/lib/index.js +6 -0
  65. package/lib/oauth-client/atproto-token-response.d.ts +100 -0
  66. package/lib/oauth-client/atproto-token-response.js +15 -0
  67. package/lib/oauth-client/constants.d.ts +4 -0
  68. package/lib/oauth-client/constants.js +4 -0
  69. package/lib/oauth-client/errors/auth-method-unsatisfiable-error.d.ts +2 -0
  70. package/lib/oauth-client/errors/auth-method-unsatisfiable-error.js +2 -0
  71. package/lib/oauth-client/errors/token-invalid-error.d.ts +6 -0
  72. package/lib/oauth-client/errors/token-invalid-error.js +6 -0
  73. package/lib/oauth-client/errors/token-refresh-error.d.ts +6 -0
  74. package/lib/oauth-client/errors/token-refresh-error.js +6 -0
  75. package/lib/oauth-client/errors/token-revoked-error.d.ts +6 -0
  76. package/lib/oauth-client/errors/token-revoked-error.js +6 -0
  77. package/lib/oauth-client/fetch-dpop.d.ts +19 -0
  78. package/lib/oauth-client/fetch-dpop.js +176 -0
  79. package/lib/oauth-client/identity-resolver.d.ts +15 -0
  80. package/lib/oauth-client/identity-resolver.js +33 -0
  81. package/lib/oauth-client/index.d.ts +17 -0
  82. package/lib/oauth-client/index.js +17 -0
  83. package/lib/oauth-client/lock.d.ts +2 -0
  84. package/lib/oauth-client/lock.js +28 -0
  85. package/lib/oauth-client/oauth-authorization-server-metadata-resolver.d.ts +18 -0
  86. package/lib/oauth-client/oauth-authorization-server-metadata-resolver.js +53 -0
  87. package/lib/oauth-client/oauth-callback-error.d.ts +6 -0
  88. package/lib/oauth-client/oauth-callback-error.js +13 -0
  89. package/lib/oauth-client/oauth-client-auth.d.ts +22 -0
  90. package/lib/oauth-client/oauth-client-auth.js +127 -0
  91. package/lib/oauth-client/oauth-client.d.ts +311 -0
  92. package/lib/oauth-client/oauth-client.js +276 -0
  93. package/lib/oauth-client/oauth-protected-resource-metadata-resolver.d.ts +18 -0
  94. package/lib/oauth-client/oauth-protected-resource-metadata-resolver.js +49 -0
  95. package/lib/oauth-client/oauth-resolver-error.d.ts +6 -0
  96. package/lib/oauth-client/oauth-resolver-error.js +18 -0
  97. package/lib/oauth-client/oauth-resolver.d.ts +71 -0
  98. package/lib/oauth-client/oauth-resolver.js +117 -0
  99. package/lib/oauth-client/oauth-response-error.d.ts +10 -0
  100. package/lib/oauth-client/oauth-response-error.js +22 -0
  101. package/lib/oauth-client/oauth-server-agent.d.ts +54 -0
  102. package/lib/oauth-client/oauth-server-agent.js +250 -0
  103. package/lib/oauth-client/oauth-server-factory.d.ts +32 -0
  104. package/lib/oauth-client/oauth-server-factory.js +37 -0
  105. package/lib/oauth-client/oauth-session.d.ts +33 -0
  106. package/lib/oauth-client/oauth-session.js +122 -0
  107. package/lib/oauth-client/runtime-implementation.d.ts +16 -0
  108. package/lib/oauth-client/runtime-implementation.js +1 -0
  109. package/lib/oauth-client/runtime.d.ts +25 -0
  110. package/lib/oauth-client/runtime.js +99 -0
  111. package/lib/oauth-client/session-getter.d.ts +54 -0
  112. package/lib/oauth-client/session-getter.js +260 -0
  113. package/lib/oauth-client/state-store.d.ts +12 -0
  114. package/lib/oauth-client/state-store.js +1 -0
  115. package/lib/oauth-client/types.d.ts +1365 -0
  116. package/lib/oauth-client/types.js +8 -0
  117. package/lib/oauth-client/util.d.ts +25 -0
  118. package/lib/oauth-client/util.js +139 -0
  119. package/lib/oauth-client/validate-client-metadata.d.ts +4 -0
  120. package/lib/oauth-client/validate-client-metadata.js +68 -0
  121. package/lib/oauth-client.d.ts +27 -0
  122. package/lib/oauth-client.js +30 -0
  123. package/lib/resolve-txt-factory.d.ts +3 -0
  124. package/lib/resolve-txt-factory.js +80 -0
  125. package/lib/session-store-kv.d.ts +9 -0
  126. package/lib/session-store-kv.js +20 -0
  127. package/lib/state-store-kv.d.ts +9 -0
  128. package/lib/state-store-kv.js +20 -0
  129. package/lib/util.d.ts +18 -0
  130. package/lib/util.js +5 -0
  131. package/package.json +58 -0
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Extract the raw, un-validated, Atproto handle from a DID document.
3
+ */
4
+ export function extractAtprotoHandle(document) {
5
+ if (document.alsoKnownAs) {
6
+ for (const h of document.alsoKnownAs) {
7
+ if (h.startsWith("at://")) {
8
+ // strip off "at://" prefix
9
+ return h.slice(5);
10
+ }
11
+ }
12
+ }
13
+ return undefined;
14
+ }
15
+ /**
16
+ * Extracts a validated, normalized Atproto handle from a DID document.
17
+ */
18
+ export function extractNormalizedHandle(document) {
19
+ const handle = extractAtprotoHandle(document);
20
+ if (!handle)
21
+ return undefined;
22
+ return asNormalizedHandle(handle);
23
+ }
24
+ export function asNormalizedHandle(input) {
25
+ const handle = normalizeHandle(input);
26
+ return isValidHandle(handle) ? handle : undefined;
27
+ }
28
+ export function normalizeHandle(handle) {
29
+ return handle.toLowerCase();
30
+ }
31
+ export function isValidHandle(handle) {
32
+ return (handle.length > 0 &&
33
+ handle.length < 254 &&
34
+ /^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/.test(handle));
35
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ export * from "./oauth-client.js";
2
+ export * from "./handle-resolver.js";
3
+ export * from "./handle-cache-kv.js";
4
+ export * from "./did-cache-kv.js";
5
+ export type { WorkersSavedState, WorkersSavedStateStore, WorkersSavedSession, WorkersSavedSessionStore, } from "./dpop-store.js";
6
+ export * from "./session-store-kv.js";
7
+ export * from "./state-store-kv.js";
package/lib/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export * from "./oauth-client.js";
2
+ export * from "./handle-resolver.js";
3
+ export * from "./handle-cache-kv.js";
4
+ export * from "./did-cache-kv.js";
5
+ export * from "./session-store-kv.js";
6
+ export * from "./state-store-kv.js";
@@ -0,0 +1,100 @@
1
+ import { TypeOf, z } from "zod";
2
+ import { SpaceSeparatedValue } from "./util.js";
3
+ export type AtprotoScope = SpaceSeparatedValue<"atproto">;
4
+ export declare const isAtprotoScope: (input: string) => input is AtprotoScope;
5
+ export declare const atprotoScopeSchema: z.ZodEffects<z.ZodString, AtprotoScope, string>;
6
+ export declare const atprotoTokenResponseSchema: z.ZodObject<{
7
+ access_token: z.ZodString;
8
+ refresh_token: z.ZodOptional<z.ZodString>;
9
+ expires_in: z.ZodOptional<z.ZodNumber>;
10
+ authorization_details: z.ZodOptional<z.ZodArray<z.ZodObject<{
11
+ type: z.ZodString;
12
+ locations: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, `${string}:${string}`, string>, "many">>;
13
+ actions: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
14
+ datatypes: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
15
+ identifier: z.ZodOptional<z.ZodString>;
16
+ privileges: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
17
+ }, "strip", z.ZodTypeAny, {
18
+ type: string;
19
+ locations?: `${string}:${string}`[] | undefined;
20
+ actions?: string[] | undefined;
21
+ datatypes?: string[] | undefined;
22
+ identifier?: string | undefined;
23
+ privileges?: string[] | undefined;
24
+ }, {
25
+ type: string;
26
+ locations?: string[] | undefined;
27
+ actions?: string[] | undefined;
28
+ datatypes?: string[] | undefined;
29
+ identifier?: string | undefined;
30
+ privileges?: string[] | undefined;
31
+ }>, "many">>;
32
+ } & {
33
+ token_type: z.ZodLiteral<"DPoP">;
34
+ sub: z.ZodEffects<z.ZodString, `did:plc:${string}` | `did:web:${string}`, string>;
35
+ scope: z.ZodEffects<z.ZodString, AtprotoScope, string>;
36
+ id_token: z.ZodOptional<z.ZodNever>;
37
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
38
+ access_token: z.ZodString;
39
+ refresh_token: z.ZodOptional<z.ZodString>;
40
+ expires_in: z.ZodOptional<z.ZodNumber>;
41
+ authorization_details: z.ZodOptional<z.ZodArray<z.ZodObject<{
42
+ type: z.ZodString;
43
+ locations: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, `${string}:${string}`, string>, "many">>;
44
+ actions: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
45
+ datatypes: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
46
+ identifier: z.ZodOptional<z.ZodString>;
47
+ privileges: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
48
+ }, "strip", z.ZodTypeAny, {
49
+ type: string;
50
+ locations?: `${string}:${string}`[] | undefined;
51
+ actions?: string[] | undefined;
52
+ datatypes?: string[] | undefined;
53
+ identifier?: string | undefined;
54
+ privileges?: string[] | undefined;
55
+ }, {
56
+ type: string;
57
+ locations?: string[] | undefined;
58
+ actions?: string[] | undefined;
59
+ datatypes?: string[] | undefined;
60
+ identifier?: string | undefined;
61
+ privileges?: string[] | undefined;
62
+ }>, "many">>;
63
+ } & {
64
+ token_type: z.ZodLiteral<"DPoP">;
65
+ sub: z.ZodEffects<z.ZodString, `did:plc:${string}` | `did:web:${string}`, string>;
66
+ scope: z.ZodEffects<z.ZodString, AtprotoScope, string>;
67
+ id_token: z.ZodOptional<z.ZodNever>;
68
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
69
+ access_token: z.ZodString;
70
+ refresh_token: z.ZodOptional<z.ZodString>;
71
+ expires_in: z.ZodOptional<z.ZodNumber>;
72
+ authorization_details: z.ZodOptional<z.ZodArray<z.ZodObject<{
73
+ type: z.ZodString;
74
+ locations: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, `${string}:${string}`, string>, "many">>;
75
+ actions: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
76
+ datatypes: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
77
+ identifier: z.ZodOptional<z.ZodString>;
78
+ privileges: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
79
+ }, "strip", z.ZodTypeAny, {
80
+ type: string;
81
+ locations?: `${string}:${string}`[] | undefined;
82
+ actions?: string[] | undefined;
83
+ datatypes?: string[] | undefined;
84
+ identifier?: string | undefined;
85
+ privileges?: string[] | undefined;
86
+ }, {
87
+ type: string;
88
+ locations?: string[] | undefined;
89
+ actions?: string[] | undefined;
90
+ datatypes?: string[] | undefined;
91
+ identifier?: string | undefined;
92
+ privileges?: string[] | undefined;
93
+ }>, "many">>;
94
+ } & {
95
+ token_type: z.ZodLiteral<"DPoP">;
96
+ sub: z.ZodEffects<z.ZodString, `did:plc:${string}` | `did:web:${string}`, string>;
97
+ scope: z.ZodEffects<z.ZodString, AtprotoScope, string>;
98
+ id_token: z.ZodOptional<z.ZodNever>;
99
+ }, z.ZodTypeAny, "passthrough">>;
100
+ export type AtprotoTokenResponse = TypeOf<typeof atprotoTokenResponseSchema>;
@@ -0,0 +1,15 @@
1
+ import { z } from "zod";
2
+ import { atprotoDidSchema } from "@atproto/did";
3
+ import { oauthTokenResponseSchema } from "@atproto/oauth-types";
4
+ import { includesSpaceSeparatedValue } from "./util.js";
5
+ export const isAtprotoScope = (input) => includesSpaceSeparatedValue(input, "atproto");
6
+ export const atprotoScopeSchema = z
7
+ .string()
8
+ .refine(isAtprotoScope, 'The "atproto" scope is required');
9
+ export const atprotoTokenResponseSchema = oauthTokenResponseSchema.extend({
10
+ token_type: z.literal("DPoP"),
11
+ sub: atprotoDidSchema,
12
+ scope: atprotoScopeSchema,
13
+ // OpenID is not compatible with atproto identities
14
+ id_token: z.never().optional(),
15
+ });
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Per ATProto spec (OpenID uses RS256)
3
+ */
4
+ export declare const FALLBACK_ALG = "ES256";
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Per ATProto spec (OpenID uses RS256)
3
+ */
4
+ export const FALLBACK_ALG = "ES256";
@@ -0,0 +1,2 @@
1
+ export declare class AuthMethodUnsatisfiableError extends Error {
2
+ }
@@ -0,0 +1,2 @@
1
+ export class AuthMethodUnsatisfiableError extends Error {
2
+ }
@@ -0,0 +1,6 @@
1
+ export declare class TokenInvalidError extends Error {
2
+ readonly sub: string;
3
+ constructor(sub: string, message?: string, options?: {
4
+ cause?: unknown;
5
+ });
6
+ }
@@ -0,0 +1,6 @@
1
+ export class TokenInvalidError extends Error {
2
+ constructor(sub, message = `The session for "${sub}" is invalid`, options) {
3
+ super(message, options);
4
+ this.sub = sub;
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ export declare class TokenRefreshError extends Error {
2
+ readonly sub: string;
3
+ constructor(sub: string, message: string, options?: {
4
+ cause?: unknown;
5
+ });
6
+ }
@@ -0,0 +1,6 @@
1
+ export class TokenRefreshError extends Error {
2
+ constructor(sub, message, options) {
3
+ super(message, options);
4
+ this.sub = sub;
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ export declare class TokenRevokedError extends Error {
2
+ readonly sub: string;
3
+ constructor(sub: string, message?: string, options?: {
4
+ cause?: unknown;
5
+ });
6
+ }
@@ -0,0 +1,6 @@
1
+ export class TokenRevokedError extends Error {
2
+ constructor(sub, message = `The session for "${sub}" was successfully revoked`, options) {
3
+ super(message, options);
4
+ this.sub = sub;
5
+ }
6
+ }
@@ -0,0 +1,19 @@
1
+ import { Key } from "@atproto/jwk";
2
+ import { Fetch, FetchContext } from "@atproto-labs/fetch";
3
+ import { SimpleStore } from "@atproto-labs/simple-store";
4
+ export type DpopFetchWrapperOptions<C = FetchContext> = {
5
+ key: Key;
6
+ nonces: SimpleStore<string, string>;
7
+ supportedAlgs?: string[];
8
+ sha256?: (input: string) => Promise<string>;
9
+ /**
10
+ * Is the intended server an authorization server (true) or a resource server
11
+ * (false)? Setting this may allow to avoid parsing the response body to
12
+ * determine the dpop-nonce.
13
+ *
14
+ * @default undefined
15
+ */
16
+ isAuthServer?: boolean;
17
+ fetch?: Fetch<C>;
18
+ };
19
+ export declare function dpopFetchWrapper<C = FetchContext>({ key, supportedAlgs, nonces, sha256, isAuthServer, fetch, }: DpopFetchWrapperOptions<C>): Fetch<C>;
@@ -0,0 +1,176 @@
1
+ import { base64url } from "multiformats/bases/base64";
2
+ import { cancelBody, peekJson } from "@atproto-labs/fetch";
3
+ // "undefined" in non https environments or environments without crypto
4
+ const subtle = globalThis.crypto?.subtle;
5
+ const ReadableStream = globalThis.ReadableStream;
6
+ export function dpopFetchWrapper({ key,
7
+ // @TODO we should provide a default based on specs
8
+ supportedAlgs, nonces, sha256 = typeof subtle !== "undefined" ? subtleSha256 : undefined, isAuthServer, fetch = globalThis.fetch, }) {
9
+ if (!sha256) {
10
+ throw new TypeError(`crypto.subtle is not available in this environment. Please provide a sha256 function.`);
11
+ }
12
+ // Throws if negotiation fails
13
+ const alg = negotiateAlg(key, supportedAlgs);
14
+ return async function (input, init) {
15
+ const request = init == null && input instanceof Request
16
+ ? input
17
+ : new Request(input, init);
18
+ const authorizationHeader = request.headers.get("Authorization");
19
+ const ath = authorizationHeader?.startsWith("DPoP ")
20
+ ? await sha256(authorizationHeader.slice(5))
21
+ : undefined;
22
+ const { origin } = new URL(request.url);
23
+ const htm = request.method;
24
+ const htu = buildHtu(request.url);
25
+ let initNonce;
26
+ try {
27
+ initNonce = await nonces.get(origin);
28
+ }
29
+ catch {
30
+ // Ignore get errors, we will just not send a nonce
31
+ }
32
+ const initProof = await buildProof(key, alg, htm, htu, initNonce, ath);
33
+ request.headers.set("DPoP", initProof);
34
+ const initResponse = await fetch.call(this, request);
35
+ // Make sure the response body is consumed. Either by the caller (when the
36
+ // response is returned), of if an error is thrown (catch block).
37
+ const nextNonce = initResponse.headers.get("DPoP-Nonce");
38
+ if (!nextNonce || nextNonce === initNonce) {
39
+ // No nonce was returned or it is the same as the one we sent. No need to
40
+ // update the nonce store, or retry the request.
41
+ return initResponse;
42
+ }
43
+ // Store the fresh nonce for future requests
44
+ try {
45
+ await nonces.set(origin, nextNonce);
46
+ }
47
+ catch {
48
+ // Ignore set errors
49
+ }
50
+ const shouldRetry = await isUseDpopNonceError(initResponse, isAuthServer);
51
+ if (!shouldRetry) {
52
+ // Not a "use_dpop_nonce" error, so there is no need to retry
53
+ return initResponse;
54
+ }
55
+ // If the input stream was already consumed, we cannot retry the request. A
56
+ // solution would be to clone() the request but that would bufferize the
57
+ // entire stream in memory which can lead to memory starvation. Instead, we
58
+ // will return the original response and let the calling code handle retries.
59
+ if (input === request) {
60
+ // The input request body was consumed. We cannot retry the request.
61
+ return initResponse;
62
+ }
63
+ if (ReadableStream && init?.body instanceof ReadableStream) {
64
+ // The init body was consumed. We cannot retry the request.
65
+ return initResponse;
66
+ }
67
+ // We will now retry the request with the fresh nonce.
68
+ // The initial response body must be consumed (see cancelBody's doc).
69
+ await cancelBody(initResponse, "log");
70
+ const nextProof = await buildProof(key, alg, htm, htu, nextNonce, ath);
71
+ const nextRequest = new Request(input, init);
72
+ nextRequest.headers.set("DPoP", nextProof);
73
+ const retryRequest = await fetch.call(this, nextRequest);
74
+ const retryNonce = retryRequest.headers.get("DPoP-Nonce");
75
+ if (!retryNonce || retryNonce === initNonce) {
76
+ // No nonce was returned or it is the same as the one we sent. No need to
77
+ // update the nonce store, or retry the request.
78
+ return retryRequest;
79
+ }
80
+ // Store the fresh nonce for future requests
81
+ try {
82
+ await nonces.set(origin, retryNonce);
83
+ }
84
+ catch {
85
+ // Ignore set errors
86
+ }
87
+ return retryRequest;
88
+ };
89
+ }
90
+ /**
91
+ * Strip query and fragment
92
+ *
93
+ * @see {@link https://www.rfc-editor.org/rfc/rfc9449.html#section-4.2-4.6}
94
+ */
95
+ function buildHtu(url) {
96
+ const fragmentIndex = url.indexOf("#");
97
+ const queryIndex = url.indexOf("?");
98
+ const end = fragmentIndex === -1
99
+ ? queryIndex
100
+ : queryIndex === -1
101
+ ? fragmentIndex
102
+ : Math.min(fragmentIndex, queryIndex);
103
+ return end === -1 ? url : url.slice(0, end);
104
+ }
105
+ async function buildProof(key, alg, htm, htu, nonce, ath) {
106
+ const jwk = key.bareJwk;
107
+ if (!jwk) {
108
+ throw new Error("Only asymmetric keys can be used as DPoP proofs");
109
+ }
110
+ const now = Math.floor(Date.now() / 1e3);
111
+ return key.createJwt(
112
+ // https://datatracker.ietf.org/doc/html/rfc9449#section-4.2
113
+ {
114
+ alg,
115
+ typ: "dpop+jwt",
116
+ jwk,
117
+ }, {
118
+ iat: now,
119
+ // Any collision will cause the request to be rejected by the server. no biggie.
120
+ jti: Math.random().toString(36).slice(2),
121
+ htm,
122
+ htu,
123
+ nonce,
124
+ ath,
125
+ });
126
+ }
127
+ async function isUseDpopNonceError(response, isAuthServer) {
128
+ // https://datatracker.ietf.org/doc/html/rfc6750#section-3
129
+ // https://datatracker.ietf.org/doc/html/rfc9449#name-resource-server-provided-no
130
+ if (isAuthServer === undefined || isAuthServer === false) {
131
+ if (response.status === 401) {
132
+ const wwwAuth = response.headers.get("WWW-Authenticate");
133
+ if (wwwAuth?.startsWith("DPoP")) {
134
+ return wwwAuth.includes('error="use_dpop_nonce"');
135
+ }
136
+ }
137
+ }
138
+ // https://datatracker.ietf.org/doc/html/rfc9449#name-authorization-server-provid
139
+ if (isAuthServer === undefined || isAuthServer === true) {
140
+ if (response.status === 400) {
141
+ try {
142
+ const json = await peekJson(response, 10 * 1024);
143
+ return (typeof json === "object" &&
144
+ json?.["error"] === "use_dpop_nonce");
145
+ }
146
+ catch {
147
+ // Response too big (to be "use_dpop_nonce" error) or invalid JSON
148
+ return false;
149
+ }
150
+ }
151
+ }
152
+ return false;
153
+ }
154
+ function negotiateAlg(key, supportedAlgs) {
155
+ if (supportedAlgs) {
156
+ // Use order of supportedAlgs as preference
157
+ const alg = supportedAlgs.find((a) => key.algorithms.includes(a));
158
+ if (alg)
159
+ return alg;
160
+ }
161
+ else {
162
+ const [alg] = key.algorithms;
163
+ if (alg)
164
+ return alg;
165
+ }
166
+ throw new Error("Key does not match any alg supported by the server");
167
+ }
168
+ async function subtleSha256(input) {
169
+ if (subtle == null) {
170
+ throw new Error(`crypto.subtle is not available in this environment. Please provide a sha256 function.`);
171
+ }
172
+ const bytes = new TextEncoder().encode(input);
173
+ const digest = await subtle.digest("SHA-256", bytes);
174
+ const digestBytes = new Uint8Array(digest);
175
+ return base64url.baseEncode(digestBytes);
176
+ }
@@ -0,0 +1,15 @@
1
+ import { DidCache, DidResolver, type DidResolverCommonOptions } from "#did-resolver";
2
+ import { HandleCache, HandleResolver, XrpcHandleResolverOptions } from "#handle-resolver";
3
+ import { IdentityResolver } from "#identity-resolver";
4
+ export type IdentityResolverOptions = {
5
+ identityResolver?: IdentityResolver;
6
+ } & Partial<DidResolverOptions & HandleResolverOptions>;
7
+ export declare function createIdentityResolver(options: IdentityResolverOptions): IdentityResolver;
8
+ export type DidResolverOptions = {
9
+ didResolver?: DidResolver<"plc" | "web">;
10
+ didCache?: DidCache;
11
+ } & Partial<DidResolverCommonOptions>;
12
+ export type HandleResolverOptions = {
13
+ handleCache?: HandleCache;
14
+ handleResolver?: URL | string | HandleResolver;
15
+ } & Partial<XrpcHandleResolverOptions>;
@@ -0,0 +1,33 @@
1
+ import { DidResolverCached, DidResolverCommon, } from "#did-resolver";
2
+ import { CachedHandleResolver, XrpcHandleResolver, } from "#handle-resolver";
3
+ import { AtprotoIdentityResolver } from "#identity-resolver";
4
+ export function createIdentityResolver(options) {
5
+ const { identityResolver } = options;
6
+ if (identityResolver != null)
7
+ return identityResolver;
8
+ const didResolver = createDidResolver(options);
9
+ const handleResolver = createHandleResolver(options);
10
+ return new AtprotoIdentityResolver(didResolver, handleResolver);
11
+ }
12
+ function createDidResolver(options) {
13
+ const { didResolver, didCache } = options;
14
+ if (didResolver instanceof DidResolverCached && !didCache) {
15
+ return didResolver;
16
+ }
17
+ return new DidResolverCached(didResolver ?? new DidResolverCommon(options), didCache);
18
+ }
19
+ function createHandleResolver(options) {
20
+ const { handleResolver, handleCache } = options;
21
+ if (handleResolver == null) {
22
+ // Because the handle resolution mechanism requires either a DNS based
23
+ // handle resolver or an XRPC based handle resolver, we require the
24
+ // handleResolver option to be provided.
25
+ throw new TypeError("handleResolver is required");
26
+ }
27
+ if (handleResolver instanceof CachedHandleResolver && !handleCache) {
28
+ return handleResolver;
29
+ }
30
+ return new CachedHandleResolver(typeof handleResolver === "string" || handleResolver instanceof URL
31
+ ? new XrpcHandleResolver(handleResolver, options)
32
+ : handleResolver, handleCache);
33
+ }
@@ -0,0 +1,17 @@
1
+ export * from "./lock.js";
2
+ export * from "./oauth-authorization-server-metadata-resolver.js";
3
+ export * from "./oauth-callback-error.js";
4
+ export * from "./oauth-client.js";
5
+ export * from "./oauth-protected-resource-metadata-resolver.js";
6
+ export * from "./oauth-resolver-error.js";
7
+ export * from "./oauth-response-error.js";
8
+ export * from "./oauth-server-agent.js";
9
+ export * from "./oauth-server-factory.js";
10
+ export * from "./oauth-session.js";
11
+ export * from "./runtime-implementation.js";
12
+ export * from "./session-getter.js";
13
+ export * from "./state-store.js";
14
+ export * from "./types.js";
15
+ export * from "./errors/token-invalid-error.js";
16
+ export * from "./errors/token-refresh-error.js";
17
+ export * from "./errors/token-revoked-error.js";
@@ -0,0 +1,17 @@
1
+ export * from "./lock.js";
2
+ export * from "./oauth-authorization-server-metadata-resolver.js";
3
+ export * from "./oauth-callback-error.js";
4
+ export * from "./oauth-client.js";
5
+ export * from "./oauth-protected-resource-metadata-resolver.js";
6
+ export * from "./oauth-resolver-error.js";
7
+ export * from "./oauth-response-error.js";
8
+ export * from "./oauth-server-agent.js";
9
+ export * from "./oauth-server-factory.js";
10
+ export * from "./oauth-session.js";
11
+ export * from "./runtime-implementation.js";
12
+ export * from "./session-getter.js";
13
+ export * from "./state-store.js";
14
+ export * from "./types.js";
15
+ export * from "./errors/token-invalid-error.js";
16
+ export * from "./errors/token-refresh-error.js";
17
+ export * from "./errors/token-revoked-error.js";
@@ -0,0 +1,2 @@
1
+ import { RuntimeLock } from "./runtime-implementation.js";
2
+ export declare const requestLocalLock: RuntimeLock;
@@ -0,0 +1,28 @@
1
+ const locks = new Map();
2
+ function acquireLocalLock(name) {
3
+ return new Promise((resolveAcquire) => {
4
+ const prev = locks.get(name) ?? Promise.resolve();
5
+ const next = prev.then(() => {
6
+ return new Promise((resolveRelease) => {
7
+ const release = () => {
8
+ // Only delete the lock if it is still the current one
9
+ if (locks.get(name) === next)
10
+ locks.delete(name);
11
+ resolveRelease();
12
+ };
13
+ resolveAcquire(release);
14
+ });
15
+ });
16
+ locks.set(name, next);
17
+ });
18
+ }
19
+ export const requestLocalLock = (name, fn) => {
20
+ return acquireLocalLock(name).then(async (release) => {
21
+ try {
22
+ return await fn();
23
+ }
24
+ finally {
25
+ release();
26
+ }
27
+ });
28
+ };
@@ -0,0 +1,18 @@
1
+ import { OAuthAuthorizationServerMetadata } from "@atproto/oauth-types";
2
+ import { Fetch } from "@atproto-labs/fetch";
3
+ import { CachedGetter, GetCachedOptions, SimpleStore } from "@atproto-labs/simple-store";
4
+ export type { GetCachedOptions, OAuthAuthorizationServerMetadata };
5
+ export type AuthorizationServerMetadataCache = SimpleStore<string, OAuthAuthorizationServerMetadata>;
6
+ export type OAuthAuthorizationServerMetadataResolverConfig = {
7
+ allowHttpIssuer?: boolean;
8
+ };
9
+ /**
10
+ * @see {@link https://datatracker.ietf.org/doc/html/rfc8414}
11
+ */
12
+ export declare class OAuthAuthorizationServerMetadataResolver extends CachedGetter<string, OAuthAuthorizationServerMetadata> {
13
+ private readonly fetch;
14
+ private readonly allowHttpIssuer;
15
+ constructor(cache: AuthorizationServerMetadataCache, fetch?: Fetch, config?: OAuthAuthorizationServerMetadataResolverConfig);
16
+ get(input: string, options?: GetCachedOptions): Promise<OAuthAuthorizationServerMetadata>;
17
+ private fetchMetadata;
18
+ }
@@ -0,0 +1,53 @@
1
+ import { oauthAuthorizationServerMetadataValidator, oauthIssuerIdentifierSchema, } from "@atproto/oauth-types";
2
+ import { FetchResponseError, bindFetch, cancelBody, } from "@atproto-labs/fetch";
3
+ import { CachedGetter, } from "@atproto-labs/simple-store";
4
+ import { contentMime } from "./util.js";
5
+ /**
6
+ * @see {@link https://datatracker.ietf.org/doc/html/rfc8414}
7
+ */
8
+ export class OAuthAuthorizationServerMetadataResolver extends CachedGetter {
9
+ constructor(cache, fetch, config) {
10
+ super(async (issuer, options) => this.fetchMetadata(issuer, options), cache);
11
+ this.fetch = bindFetch(fetch);
12
+ this.allowHttpIssuer = config?.allowHttpIssuer === true;
13
+ }
14
+ async get(input, options) {
15
+ const issuer = oauthIssuerIdentifierSchema.parse(input);
16
+ if (!this.allowHttpIssuer && issuer.startsWith("http:")) {
17
+ throw new TypeError("Unsecure issuer URL protocol only allowed in development and test environments");
18
+ }
19
+ return super.get(issuer, options);
20
+ }
21
+ async fetchMetadata(issuer, options) {
22
+ const url = new URL(`/.well-known/oauth-authorization-server`, issuer);
23
+ const request = new Request(url, {
24
+ headers: { accept: "application/json", "cache-control": "no-cache" },
25
+ // cache: options?.noCache ? "no-cache" : undefined,
26
+ signal: options?.signal,
27
+ redirect: "manual", // response must be 200 OK
28
+ });
29
+ const response = await this.fetch(request);
30
+ // https://datatracker.ietf.org/doc/html/rfc8414#section-3.2
31
+ if (response.status !== 200) {
32
+ await cancelBody(response, "log");
33
+ throw await FetchResponseError.from(response, `Unexpected status code ${response.status} for "${url}"`, undefined, { cause: request });
34
+ }
35
+ if (contentMime(response.headers) !== "application/json") {
36
+ await cancelBody(response, "log");
37
+ throw await FetchResponseError.from(response, `Unexpected content type for "${url}"`, undefined, { cause: request });
38
+ }
39
+ const metadata = oauthAuthorizationServerMetadataValidator.parse(await response.json());
40
+ // Validate the issuer (MIX-UP attacks)
41
+ // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#name-mix-up-attacks
42
+ // https://datatracker.ietf.org/doc/html/rfc8414#section-2
43
+ if (metadata.issuer !== issuer) {
44
+ throw new TypeError(`Invalid issuer ${metadata.issuer}`);
45
+ }
46
+ // ATPROTO requires client_id_metadata_document
47
+ // http://drafts.aaronpk.com/draft-parecki-oauth-client-id-metadata-document/draft-parecki-oauth-client-id-metadata-document.html
48
+ if (metadata.client_id_metadata_document_supported !== true) {
49
+ throw new TypeError(`Authorization server "${issuer}" does not support client_id_metadata_document`);
50
+ }
51
+ return metadata;
52
+ }
53
+ }
@@ -0,0 +1,6 @@
1
+ export declare class OAuthCallbackError extends Error {
2
+ readonly params: URLSearchParams;
3
+ readonly state?: string | undefined;
4
+ static from(err: unknown, params: URLSearchParams, state?: string): OAuthCallbackError;
5
+ constructor(params: URLSearchParams, message?: string, state?: string | undefined, cause?: unknown);
6
+ }
@@ -0,0 +1,13 @@
1
+ export class OAuthCallbackError extends Error {
2
+ static from(err, params, state) {
3
+ if (err instanceof OAuthCallbackError)
4
+ return err;
5
+ const message = err instanceof Error ? err.message : undefined;
6
+ return new OAuthCallbackError(params, message, state, err);
7
+ }
8
+ constructor(params, message = params.get("error_description") || "OAuth callback error", state, cause) {
9
+ super(message, { cause });
10
+ this.params = params;
11
+ this.state = state;
12
+ }
13
+ }