@atproto/oauth-types 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/atproto-loopback-client-id.d.ts +14 -0
  3. package/dist/atproto-loopback-client-id.d.ts.map +1 -0
  4. package/dist/atproto-loopback-client-id.js +43 -0
  5. package/dist/atproto-loopback-client-id.js.map +1 -0
  6. package/dist/atproto-loopback-client-metadata.d.ts +8 -1
  7. package/dist/atproto-loopback-client-metadata.d.ts.map +1 -1
  8. package/dist/atproto-loopback-client-metadata.js +13 -4
  9. package/dist/atproto-loopback-client-metadata.js.map +1 -1
  10. package/dist/atproto-loopback-client-redirect-uris.d.ts +2 -0
  11. package/dist/atproto-loopback-client-redirect-uris.d.ts.map +1 -0
  12. package/dist/atproto-loopback-client-redirect-uris.js +8 -0
  13. package/dist/atproto-loopback-client-redirect-uris.js.map +1 -0
  14. package/dist/atproto-oauth-scope.d.ts +12 -0
  15. package/dist/atproto-oauth-scope.d.ts.map +1 -0
  16. package/dist/atproto-oauth-scope.js +27 -0
  17. package/dist/atproto-oauth-scope.js.map +1 -0
  18. package/dist/atproto-oauth-token-response.d.ts +106 -0
  19. package/dist/atproto-oauth-token-response.d.ts.map +1 -0
  20. package/dist/atproto-oauth-token-response.js +15 -0
  21. package/dist/atproto-oauth-token-response.js.map +1 -0
  22. package/dist/constants.js.map +1 -1
  23. package/dist/index.d.ts +5 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +5 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/oauth-access-token.js.map +1 -1
  28. package/dist/oauth-authorization-code-grant-token-request.js.map +1 -1
  29. package/dist/oauth-authorization-details.js.map +1 -1
  30. package/dist/oauth-authorization-request-jar.js.map +1 -1
  31. package/dist/oauth-authorization-request-par.d.ts +12 -12
  32. package/dist/oauth-authorization-request-par.js.map +1 -1
  33. package/dist/oauth-authorization-request-parameters.d.ts +12 -12
  34. package/dist/oauth-authorization-request-parameters.js.map +1 -1
  35. package/dist/oauth-authorization-request-query.d.ts +12 -12
  36. package/dist/oauth-authorization-request-query.js.map +1 -1
  37. package/dist/oauth-authorization-request-uri.js.map +1 -1
  38. package/dist/oauth-authorization-response-error.js.map +1 -1
  39. package/dist/oauth-authorization-server-metadata.js +2 -2
  40. package/dist/oauth-authorization-server-metadata.js.map +1 -1
  41. package/dist/oauth-client-credentials-grant-token-request.js.map +1 -1
  42. package/dist/oauth-client-credentials.js.map +1 -1
  43. package/dist/oauth-client-id-discoverable.d.ts +1 -1
  44. package/dist/oauth-client-id-discoverable.js +1 -1
  45. package/dist/oauth-client-id-discoverable.js.map +1 -1
  46. package/dist/oauth-client-id-loopback.d.ts +24 -8
  47. package/dist/oauth-client-id-loopback.d.ts.map +1 -1
  48. package/dist/oauth-client-id-loopback.js +97 -60
  49. package/dist/oauth-client-id-loopback.js.map +1 -1
  50. package/dist/oauth-client-id.js.map +1 -1
  51. package/dist/oauth-client-metadata.d.ts +160 -1098
  52. package/dist/oauth-client-metadata.d.ts.map +1 -1
  53. package/dist/oauth-client-metadata.js.map +1 -1
  54. package/dist/oauth-code-challenge-method.js.map +1 -1
  55. package/dist/oauth-endpoint-auth-method.js.map +1 -1
  56. package/dist/oauth-endpoint-name.js.map +1 -1
  57. package/dist/oauth-grant-type.js.map +1 -1
  58. package/dist/oauth-introspection-response.js.map +1 -1
  59. package/dist/oauth-issuer-identifier.js.map +1 -1
  60. package/dist/oauth-par-response.d.ts +2 -2
  61. package/dist/oauth-par-response.js.map +1 -1
  62. package/dist/oauth-password-grant-token-request.js.map +1 -1
  63. package/dist/oauth-protected-resource-metadata.d.ts +1 -1
  64. package/dist/oauth-protected-resource-metadata.js +1 -1
  65. package/dist/oauth-protected-resource-metadata.js.map +1 -1
  66. package/dist/oauth-redirect-uri.d.ts +18 -6
  67. package/dist/oauth-redirect-uri.d.ts.map +1 -1
  68. package/dist/oauth-redirect-uri.js +18 -19
  69. package/dist/oauth-redirect-uri.js.map +1 -1
  70. package/dist/oauth-refresh-token-grant-token-request.js.map +1 -1
  71. package/dist/oauth-refresh-token.js.map +1 -1
  72. package/dist/oauth-request-uri.js.map +1 -1
  73. package/dist/oauth-response-mode.js.map +1 -1
  74. package/dist/oauth-response-type.js.map +1 -1
  75. package/dist/oauth-scope.d.ts +5 -3
  76. package/dist/oauth-scope.d.ts.map +1 -1
  77. package/dist/oauth-scope.js +11 -8
  78. package/dist/oauth-scope.js.map +1 -1
  79. package/dist/oauth-token-identification.js.map +1 -1
  80. package/dist/oauth-token-request.js.map +1 -1
  81. package/dist/oauth-token-response.js.map +1 -1
  82. package/dist/oauth-token-type.js.map +1 -1
  83. package/dist/oidc-authorization-error-response.js.map +1 -1
  84. package/dist/oidc-claims-parameter.js.map +1 -1
  85. package/dist/oidc-claims-properties.js.map +1 -1
  86. package/dist/oidc-entity-type.js.map +1 -1
  87. package/dist/oidc-userinfo.js.map +1 -1
  88. package/dist/uri.d.ts.map +1 -1
  89. package/dist/uri.js +44 -17
  90. package/dist/uri.js.map +1 -1
  91. package/dist/util.d.ts +11 -1
  92. package/dist/util.d.ts.map +1 -1
  93. package/dist/util.js +74 -5
  94. package/dist/util.js.map +1 -1
  95. package/package.json +3 -2
  96. package/src/atproto-loopback-client-id.ts +78 -0
  97. package/src/atproto-loopback-client-metadata.ts +33 -13
  98. package/src/atproto-loopback-client-redirect-uris.ts +4 -0
  99. package/src/atproto-oauth-scope.ts +34 -0
  100. package/src/atproto-oauth-token-response.ts +16 -0
  101. package/src/index.ts +5 -1
  102. package/src/oauth-authorization-server-metadata.ts +2 -2
  103. package/src/oauth-client-id-discoverable.ts +1 -1
  104. package/src/oauth-client-id-loopback.ts +131 -73
  105. package/src/oauth-protected-resource-metadata.ts +1 -1
  106. package/src/oauth-redirect-uri.ts +20 -26
  107. package/src/oauth-scope.ts +13 -7
  108. package/src/uri.ts +54 -18
  109. package/src/util.ts +85 -3
  110. package/tsconfig.build.tsbuildinfo +1 -1
package/dist/util.d.ts CHANGED
@@ -1,9 +1,19 @@
1
+ export declare const canParseUrl: (url: string | URL, base?: string | URL) => boolean;
1
2
  export declare function isHostnameIP(hostname: string): boolean;
2
3
  export type LoopbackHost = 'localhost' | '127.0.0.1' | '[::1]';
3
4
  export declare function isLoopbackHost(host: unknown): host is LoopbackHost;
4
- export declare function isLoopbackUrl(input: URL | string): boolean;
5
+ export declare function isLocalHostname(hostname: string): boolean;
5
6
  export declare function safeUrl(input: URL | string): URL | null;
6
7
  export declare function extractUrlPath(url: any): any;
7
8
  export declare const jsonObjectPreprocess: (val: unknown) => any;
8
9
  export declare const numberPreprocess: (val: unknown) => unknown;
10
+ /**
11
+ * Returns true if the two arrays contain the same elements, regardless of order
12
+ * or duplicates.
13
+ */
14
+ export declare function arrayEquivalent<T>(a: readonly T[], b: readonly T[]): boolean;
15
+ export declare function includedIn<T>(this: readonly T[], item: T): boolean;
16
+ export declare function asArray<T>(value: Iterable<T> | undefined): undefined | readonly T[];
17
+ export type SpaceSeparatedValue<Value extends string> = `${'' | `${string} `}${Value}${'' | ` ${string}`}`;
18
+ export declare const isSpaceSeparatedValue: <Value extends string>(value: Value, input: string) => input is SpaceSeparatedValue<Value>;
9
19
  //# sourceMappingURL=util.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAAA,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,WAQ5C;AAED,MAAM,MAAM,YAAY,GAAG,WAAW,GAAG,WAAW,GAAG,OAAO,CAAA;AAE9D,wBAAgB,cAAc,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,IAAI,YAAY,CAElE;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,GAAG,GAAG,MAAM,GAAG,OAAO,CAG1D;AAED,wBAAgB,OAAO,CAAC,KAAK,EAAE,GAAG,GAAG,MAAM,GAAG,GAAG,GAAG,IAAI,CAMvD;AAED,wBAAgB,cAAc,CAAC,GAAG,KAAA,OAsCjC;AAED,eAAO,MAAM,oBAAoB,GAAI,KAAK,OAAO,QAUhD,CAAA;AAED,eAAO,MAAM,gBAAgB,GAAI,KAAK,OAAO,KAAG,OAM/C,CAAA"}
1
+ {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,WAAW,qDAWpB,CAAA;AAEJ,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,WAQ5C;AAED,MAAM,MAAM,YAAY,GAAG,WAAW,GAAG,WAAW,GAAG,OAAO,CAAA;AAE9D,wBAAgB,cAAc,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,IAAI,YAAY,CAElE;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAYzD;AAED,wBAAgB,OAAO,CAAC,KAAK,EAAE,GAAG,GAAG,MAAM,GAAG,GAAG,GAAG,IAAI,CAMvD;AAED,wBAAgB,cAAc,CAAC,GAAG,KAAA,OAsCjC;AAED,eAAO,MAAM,oBAAoB,GAAI,KAAK,OAAO,QAUhD,CAAA;AAED,eAAO,MAAM,gBAAgB,GAAI,KAAK,OAAO,KAAG,OAM/C,CAAA;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,SAAS,CAAC,EAAE,EAAE,CAAC,EAAE,SAAS,CAAC,EAAE,WAGlE;AAED,wBAAgB,UAAU,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,WAExD;AAED,wBAAgB,OAAO,CAAC,CAAC,EACvB,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC,GAAG,SAAS,GAC7B,SAAS,GAAG,SAAS,CAAC,EAAE,CAI1B;AAED,MAAM,MAAM,mBAAmB,CAAC,KAAK,SAAS,MAAM,IAClD,GAAG,EAAE,GAAG,GAAG,MAAM,GAAG,GAAG,KAAK,GAAG,EAAE,GAAG,IAAI,MAAM,EAAE,EAAE,CAAA;AAEpD,eAAO,MAAM,qBAAqB,GAAI,KAAK,SAAS,MAAM,EACxD,OAAO,KAAK,EACZ,OAAO,MAAM,KACZ,KAAK,IAAI,mBAAmB,CAAC,KAAK,CA+BpC,CAAA"}
package/dist/util.js CHANGED
@@ -1,11 +1,27 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.numberPreprocess = exports.jsonObjectPreprocess = void 0;
3
+ exports.isSpaceSeparatedValue = exports.numberPreprocess = exports.jsonObjectPreprocess = exports.canParseUrl = void 0;
4
4
  exports.isHostnameIP = isHostnameIP;
5
5
  exports.isLoopbackHost = isLoopbackHost;
6
- exports.isLoopbackUrl = isLoopbackUrl;
6
+ exports.isLocalHostname = isLocalHostname;
7
7
  exports.safeUrl = safeUrl;
8
8
  exports.extractUrlPath = extractUrlPath;
9
+ exports.arrayEquivalent = arrayEquivalent;
10
+ exports.includedIn = includedIn;
11
+ exports.asArray = asArray;
12
+ exports.canParseUrl =
13
+ // eslint-disable-next-line n/no-unsupported-features/node-builtins
14
+ URL.canParse?.bind(URL) ??
15
+ // URL.canParse is not available in Node.js < 18.7.0
16
+ ((urlStr) => {
17
+ try {
18
+ new URL(urlStr);
19
+ return true;
20
+ }
21
+ catch {
22
+ return false;
23
+ }
24
+ });
9
25
  function isHostnameIP(hostname) {
10
26
  // IPv4
11
27
  if (hostname.match(/^\d+\.\d+\.\d+\.\d+$/))
@@ -18,9 +34,16 @@ function isHostnameIP(hostname) {
18
34
  function isLoopbackHost(host) {
19
35
  return host === 'localhost' || host === '127.0.0.1' || host === '[::1]';
20
36
  }
21
- function isLoopbackUrl(input) {
22
- const url = typeof input === 'string' ? new URL(input) : input;
23
- return isLoopbackHost(url.hostname);
37
+ function isLocalHostname(hostname) {
38
+ const parts = hostname.split('.');
39
+ if (parts.length < 2)
40
+ return true;
41
+ const tld = parts.at(-1).toLowerCase();
42
+ return (tld === 'test' ||
43
+ tld === 'local' ||
44
+ tld === 'localhost' ||
45
+ tld === 'invalid' ||
46
+ tld === 'example');
24
47
  }
25
48
  function safeUrl(input) {
26
49
  try {
@@ -81,4 +104,50 @@ const numberPreprocess = (val) => {
81
104
  return val;
82
105
  };
83
106
  exports.numberPreprocess = numberPreprocess;
107
+ /**
108
+ * Returns true if the two arrays contain the same elements, regardless of order
109
+ * or duplicates.
110
+ */
111
+ function arrayEquivalent(a, b) {
112
+ if (a === b)
113
+ return true;
114
+ return a.every(includedIn, b) && b.every(includedIn, a);
115
+ }
116
+ function includedIn(item) {
117
+ return this.includes(item);
118
+ }
119
+ function asArray(value) {
120
+ if (value == null)
121
+ return undefined;
122
+ if (Array.isArray(value))
123
+ return value; // already a (possibly readonly) array
124
+ return Array.from(value);
125
+ }
126
+ const isSpaceSeparatedValue = (value, input) => {
127
+ if (value.length === 0)
128
+ throw new TypeError('Value cannot be empty');
129
+ if (value.includes(' '))
130
+ throw new TypeError('Value cannot contain spaces');
131
+ // Optimized version of:
132
+ // return input.split(' ').includes(value)
133
+ const inputLength = input.length;
134
+ const valueLength = value.length;
135
+ if (inputLength < valueLength)
136
+ return false;
137
+ let idx = input.indexOf(value);
138
+ let idxEnd;
139
+ while (idx !== -1) {
140
+ idxEnd = idx + valueLength;
141
+ if (
142
+ // at beginning or preceded by space
143
+ (idx === 0 || input.charCodeAt(idx - 1) === 32) &&
144
+ // at end or followed by space
145
+ (idxEnd === inputLength || input.charCodeAt(idxEnd) === 32)) {
146
+ return true;
147
+ }
148
+ idx = input.indexOf(value, idxEnd + 1);
149
+ }
150
+ return false;
151
+ };
152
+ exports.isSpaceSeparatedValue = isSpaceSeparatedValue;
84
153
  //# sourceMappingURL=util.js.map
package/dist/util.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"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","sourcesContent":["export const canParseUrl =\n // eslint-disable-next-line n/no-unsupported-features/node-builtins\n URL.canParse?.bind(URL) ??\n // URL.canParse is not available in Node.js < 18.7.0\n ((urlStr: string): boolean => {\n try {\n new URL(urlStr)\n return true\n } catch {\n return false\n }\n })\n\nexport function isHostnameIP(hostname: string) {\n // IPv4\n if (hostname.match(/^\\d+\\.\\d+\\.\\d+\\.\\d+$/)) return true\n\n // IPv6\n if (hostname.startsWith('[') && hostname.endsWith(']')) return true\n\n return false\n}\n\nexport type LoopbackHost = 'localhost' | '127.0.0.1' | '[::1]'\n\nexport function isLoopbackHost(host: unknown): host is LoopbackHost {\n return host === 'localhost' || host === '127.0.0.1' || host === '[::1]'\n}\n\nexport function isLocalHostname(hostname: string): boolean {\n const parts = hostname.split('.')\n if (parts.length < 2) return true\n\n const tld = parts.at(-1)!.toLowerCase()\n return (\n tld === 'test' ||\n tld === 'local' ||\n tld === 'localhost' ||\n tld === 'invalid' ||\n tld === 'example'\n )\n}\n\nexport function safeUrl(input: URL | string): URL | null {\n try {\n return new URL(input)\n } catch {\n return null\n }\n}\n\nexport function extractUrlPath(url) {\n // Extracts the path from a URL, without relying on the URL constructor\n // (because it normalizes the URL)\n const endOfProtocol = url.startsWith('https://')\n ? 8\n : url.startsWith('http://')\n ? 7\n : -1\n if (endOfProtocol === -1) {\n throw new TypeError('URL must use the \"https:\" or \"http:\" protocol')\n }\n\n const hashIdx = url.indexOf('#', endOfProtocol)\n const questionIdx = url.indexOf('?', endOfProtocol)\n\n const queryStrIdx =\n questionIdx !== -1 && (hashIdx === -1 || questionIdx < hashIdx)\n ? questionIdx\n : -1\n\n const pathEnd =\n hashIdx === -1\n ? queryStrIdx === -1\n ? url.length\n : queryStrIdx\n : queryStrIdx === -1\n ? hashIdx\n : Math.min(hashIdx, queryStrIdx)\n\n const slashIdx = url.indexOf('/', endOfProtocol)\n\n const pathStart = slashIdx === -1 || slashIdx > pathEnd ? pathEnd : slashIdx\n\n if (endOfProtocol === pathStart) {\n throw new TypeError('URL must contain a host')\n }\n\n return url.substring(pathStart, pathEnd)\n}\n\nexport const jsonObjectPreprocess = (val: unknown) => {\n if (typeof val === 'string' && val.startsWith('{') && val.endsWith('}')) {\n try {\n return JSON.parse(val)\n } catch {\n return val\n }\n }\n\n return val\n}\n\nexport const numberPreprocess = (val: unknown): unknown => {\n if (typeof val === 'string') {\n const number = Number(val)\n if (!Number.isNaN(number)) return number\n }\n return val\n}\n\n/**\n * Returns true if the two arrays contain the same elements, regardless of order\n * or duplicates.\n */\nexport function arrayEquivalent<T>(a: readonly T[], b: readonly T[]) {\n if (a === b) return true\n return a.every(includedIn, b) && b.every(includedIn, a)\n}\n\nexport function includedIn<T>(this: readonly T[], item: T) {\n return this.includes(item)\n}\n\nexport function asArray<T>(\n value: Iterable<T> | undefined,\n): undefined | readonly T[] {\n if (value == null) return undefined\n if (Array.isArray(value)) return value // already a (possibly readonly) array\n return Array.from(value)\n}\n\nexport type SpaceSeparatedValue<Value extends string> =\n `${'' | `${string} `}${Value}${'' | ` ${string}`}`\n\nexport const isSpaceSeparatedValue = <Value extends string>(\n value: Value,\n input: string,\n): input is SpaceSeparatedValue<Value> => {\n if (value.length === 0) throw new TypeError('Value cannot be empty')\n if (value.includes(' ')) throw new TypeError('Value cannot contain spaces')\n\n // Optimized version of:\n // return input.split(' ').includes(value)\n\n const inputLength = input.length\n const valueLength = value.length\n\n if (inputLength < valueLength) return false\n\n let idx = input.indexOf(value)\n let idxEnd: number\n\n while (idx !== -1) {\n idxEnd = idx + valueLength\n\n if (\n // at beginning or preceded by space\n (idx === 0 || input.charCodeAt(idx - 1) === 32) &&\n // at end or followed by space\n (idxEnd === inputLength || input.charCodeAt(idxEnd) === 32)\n ) {\n return true\n }\n\n idx = input.indexOf(value, idxEnd + 1)\n }\n\n return false\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/oauth-types",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "license": "MIT",
5
5
  "description": "OAuth typing & validation library",
6
6
  "keywords": [
@@ -26,7 +26,8 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "zod": "^3.23.8",
29
- "@atproto/jwk": "0.5.0"
29
+ "@atproto/did": "0.2.1",
30
+ "@atproto/jwk": "0.6.0"
30
31
  },
31
32
  "devDependencies": {
32
33
  "typescript": "^5.6.3"
@@ -0,0 +1,78 @@
1
+ import { DEFAULT_LOOPBACK_CLIENT_REDIRECT_URIS } from './atproto-loopback-client-redirect-uris.js'
2
+ import {
3
+ AtprotoOAuthScope,
4
+ DEFAULT_ATPROTO_OAUTH_SCOPE,
5
+ asAtprotoOAuthScope,
6
+ isAtprotoOAuthScope,
7
+ } from './atproto-oauth-scope.js'
8
+ import {
9
+ LOOPBACK_CLIENT_ID_ORIGIN,
10
+ OAuthClientIdLoopback,
11
+ parseOAuthLoopbackClientId,
12
+ } from './oauth-client-id-loopback.js'
13
+ import {
14
+ OAuthLoopbackRedirectURI,
15
+ oauthLoopbackClientRedirectUriSchema,
16
+ } from './oauth-redirect-uri.js'
17
+ import { arrayEquivalent, asArray } from './util.js'
18
+
19
+ export type OAuthLoopbackClientIdConfig = {
20
+ scope?: string
21
+ redirect_uris?: Iterable<string>
22
+ }
23
+
24
+ export function buildAtprotoLoopbackClientId(
25
+ config?: OAuthLoopbackClientIdConfig,
26
+ ): OAuthClientIdLoopback {
27
+ if (config) {
28
+ const params = new URLSearchParams()
29
+
30
+ const { scope } = config
31
+ if (scope != null && scope !== DEFAULT_ATPROTO_OAUTH_SCOPE) {
32
+ params.set('scope', asAtprotoOAuthScope(scope))
33
+ }
34
+
35
+ const redirectUris = asArray(config.redirect_uris)
36
+ if (
37
+ redirectUris &&
38
+ !arrayEquivalent(redirectUris, DEFAULT_LOOPBACK_CLIENT_REDIRECT_URIS)
39
+ ) {
40
+ if (!redirectUris.length) {
41
+ throw new TypeError(`Unexpected empty "redirect_uris" config`)
42
+ }
43
+ for (const uri of redirectUris) {
44
+ params.append(
45
+ 'redirect_uri',
46
+ oauthLoopbackClientRedirectUriSchema.parse(uri),
47
+ )
48
+ }
49
+ }
50
+
51
+ if (params.size) {
52
+ return `${LOOPBACK_CLIENT_ID_ORIGIN}?${params.toString()}`
53
+ }
54
+ }
55
+
56
+ return LOOPBACK_CLIENT_ID_ORIGIN
57
+ }
58
+
59
+ export type AtprotoLoopbackClientIdParams = {
60
+ scope: AtprotoOAuthScope
61
+ redirect_uris: [OAuthLoopbackRedirectURI, ...OAuthLoopbackRedirectURI[]]
62
+ }
63
+
64
+ export function parseAtprotoLoopbackClientId(
65
+ clientId: string,
66
+ ): AtprotoLoopbackClientIdParams {
67
+ const { scope = DEFAULT_ATPROTO_OAUTH_SCOPE, redirect_uris } =
68
+ parseOAuthLoopbackClientId(clientId)
69
+ if (!isAtprotoOAuthScope(scope)) {
70
+ throw new TypeError(
71
+ 'ATProto Loopback ClientID must include "atproto" scope',
72
+ )
73
+ }
74
+ return {
75
+ scope,
76
+ redirect_uris: redirect_uris ?? [...DEFAULT_LOOPBACK_CLIENT_REDIRECT_URIS],
77
+ }
78
+ }
@@ -1,23 +1,43 @@
1
1
  import {
2
- OAuthClientIdLoopback,
3
- parseOAuthLoopbackClientId,
4
- } from './oauth-client-id-loopback.js'
2
+ AtprotoLoopbackClientIdParams,
3
+ OAuthLoopbackClientIdConfig,
4
+ buildAtprotoLoopbackClientId,
5
+ parseAtprotoLoopbackClientId,
6
+ } from './atproto-loopback-client-id.js'
7
+ import { AtprotoOAuthScope } from './atproto-oauth-scope.js'
8
+ import { OAuthClientIdLoopback } from './oauth-client-id-loopback.js'
5
9
  import { OAuthClientMetadataInput } from './oauth-client-metadata.js'
10
+ import { OAuthLoopbackRedirectURI } from './oauth-redirect-uri.js'
11
+
12
+ export type AtprotoLoopbackClientMetadata = OAuthClientMetadataInput & {
13
+ client_id: OAuthClientIdLoopback
14
+ scope: AtprotoOAuthScope
15
+ redirect_uris: [OAuthLoopbackRedirectURI, ...OAuthLoopbackRedirectURI[]]
16
+ }
6
17
 
7
18
  export function atprotoLoopbackClientMetadata(
8
19
  clientId: string,
9
- ): OAuthClientMetadataInput & {
10
- client_id: OAuthClientIdLoopback
11
- } {
12
- const {
13
- scope = 'atproto',
14
- redirect_uris = [`http://127.0.0.1/`, `http://[::1]/`],
15
- } = parseOAuthLoopbackClientId(clientId)
20
+ ): AtprotoLoopbackClientMetadata {
21
+ const params = parseAtprotoLoopbackClientId(clientId)
22
+ // Safe to cast because parseAtprotoLoopbackClientId ensures it's a loopback ID
23
+ return buildMetadataInternal(clientId as OAuthClientIdLoopback, params)
24
+ }
25
+
26
+ export function buildAtprotoLoopbackClientMetadata(
27
+ config: OAuthLoopbackClientIdConfig,
28
+ ): AtprotoLoopbackClientMetadata {
29
+ const clientId = buildAtprotoLoopbackClientId(config)
30
+ return buildMetadataInternal(clientId, parseAtprotoLoopbackClientId(clientId))
31
+ }
16
32
 
33
+ function buildMetadataInternal(
34
+ clientId: OAuthClientIdLoopback,
35
+ clientParams: AtprotoLoopbackClientIdParams,
36
+ ): AtprotoLoopbackClientMetadata {
17
37
  return {
18
- client_id: clientId as OAuthClientIdLoopback,
19
- scope,
20
- redirect_uris,
38
+ client_id: clientId,
39
+ scope: clientParams.scope,
40
+ redirect_uris: clientParams.redirect_uris,
21
41
  response_types: ['code'],
22
42
  grant_types: ['authorization_code', 'refresh_token'],
23
43
  token_endpoint_auth_method: 'none',
@@ -0,0 +1,4 @@
1
+ export const DEFAULT_LOOPBACK_CLIENT_REDIRECT_URIS = Object.freeze([
2
+ `http://127.0.0.1/`,
3
+ `http://[::1]/`,
4
+ ] as const)
@@ -0,0 +1,34 @@
1
+ import { z } from 'zod'
2
+ import { OAuthScope, isOAuthScope } from './oauth-scope.js'
3
+ import { SpaceSeparatedValue, isSpaceSeparatedValue } from './util.js'
4
+
5
+ export const ATPROTO_SCOPE_VALUE = 'atproto'
6
+ export type AtprotoScopeValue = typeof ATPROTO_SCOPE_VALUE
7
+
8
+ export type AtprotoOAuthScope = OAuthScope &
9
+ SpaceSeparatedValue<AtprotoScopeValue>
10
+
11
+ export function isAtprotoOAuthScope(input: string): input is AtprotoOAuthScope {
12
+ return (
13
+ isOAuthScope(input) && isSpaceSeparatedValue(ATPROTO_SCOPE_VALUE, input)
14
+ )
15
+ }
16
+
17
+ export function asAtprotoOAuthScope<I extends string>(input: I) {
18
+ if (isAtprotoOAuthScope(input)) return input
19
+ throw new TypeError(`Value must contain "${ATPROTO_SCOPE_VALUE}" scope value`)
20
+ }
21
+
22
+ export function assertAtprotoOAuthScope(
23
+ input: string,
24
+ ): asserts input is AtprotoOAuthScope {
25
+ void asAtprotoOAuthScope(input)
26
+ }
27
+
28
+ export const atprotoOAuthScopeSchema = z.string().refine(isAtprotoOAuthScope, {
29
+ message: 'Invalid ATProto OAuth scope',
30
+ })
31
+
32
+ // Default scope is for reading identity (did) only
33
+ export const DEFAULT_ATPROTO_OAUTH_SCOPE =
34
+ ATPROTO_SCOPE_VALUE satisfies AtprotoOAuthScope
@@ -0,0 +1,16 @@
1
+ import { TypeOf, z } from 'zod'
2
+ import { atprotoDidSchema } from '@atproto/did'
3
+ import { atprotoOAuthScopeSchema } from './atproto-oauth-scope'
4
+ import { oauthTokenResponseSchema } from './oauth-token-response.js'
5
+
6
+ export const atprotoOAuthTokenResponseSchema = oauthTokenResponseSchema.extend({
7
+ token_type: z.literal('DPoP'),
8
+ sub: atprotoDidSchema,
9
+ scope: atprotoOAuthScopeSchema,
10
+ // OpenID is not compatible with atproto identities
11
+ id_token: z.never().optional(),
12
+ })
13
+
14
+ export type AtprotoOAuthTokenResponse = TypeOf<
15
+ typeof atprotoOAuthTokenResponseSchema
16
+ >
package/src/index.ts CHANGED
@@ -2,9 +2,12 @@ export * from './constants.js'
2
2
  export * from './uri.js'
3
3
  export * from './util.js'
4
4
 
5
+ export * from './atproto-loopback-client-id.js'
5
6
  export * from './atproto-loopback-client-metadata.js'
7
+ export * from './atproto-loopback-client-redirect-uris.js'
8
+ export * from './atproto-oauth-scope.js'
9
+ export * from './atproto-oauth-token-response.js'
6
10
  export * from './oauth-access-token.js'
7
- export * from './oauth-authorization-response-error.js'
8
11
  export * from './oauth-authorization-code-grant-token-request.js'
9
12
  export * from './oauth-authorization-details.js'
10
13
  export * from './oauth-authorization-request-jar.js'
@@ -12,6 +15,7 @@ export * from './oauth-authorization-request-par.js'
12
15
  export * from './oauth-authorization-request-parameters.js'
13
16
  export * from './oauth-authorization-request-query.js'
14
17
  export * from './oauth-authorization-request-uri.js'
18
+ export * from './oauth-authorization-response-error.js'
15
19
  export * from './oauth-authorization-server-metadata.js'
16
20
  export * from './oauth-client-credentials-grant-token-request.js'
17
21
  export * from './oauth-client-credentials.js'
@@ -67,10 +67,10 @@ export const oauthAuthorizationServerMetadataSchema = z.object({
67
67
  // https://datatracker.ietf.org/doc/html/rfc9449#section-5.1
68
68
  dpop_signing_alg_values_supported: z.array(z.string()).optional(),
69
69
 
70
- // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-resource-metadata-05#section-4
70
+ // https://www.rfc-editor.org/rfc/rfc9728.html#section-4
71
71
  protected_resources: z.array(webUriSchema).optional(),
72
72
 
73
- // https://drafts.aaronpk.com/draft-parecki-oauth-client-id-metadata-document/draft-parecki-oauth-client-id-metadata-document.html
73
+ // https://www.ietf.org/archive/id/draft-ietf-oauth-client-id-metadata-document-00.html
74
74
  client_id_metadata_document_supported: z.boolean().optional(),
75
75
  })
76
76
 
@@ -4,7 +4,7 @@ import { httpsUriSchema } from './uri.js'
4
4
  import { extractUrlPath, isHostnameIP } from './util.js'
5
5
 
6
6
  /**
7
- * @see {@link https://drafts.aaronpk.com/draft-parecki-oauth-client-id-metadata-document/draft-parecki-oauth-client-id-metadata-document.html}
7
+ * @see {@link https://www.ietf.org/archive/id/draft-ietf-oauth-client-id-metadata-document-00.html}
8
8
  */
9
9
  export const oauthClientIdDiscoverableSchema = z
10
10
  .intersection(oauthClientIdSchema, httpsUriSchema)
@@ -1,106 +1,164 @@
1
- import { TypeOf, ZodIssueCode } from 'zod'
2
1
  import { oauthClientIdSchema } from './oauth-client-id.js'
3
2
  import {
4
3
  OAuthLoopbackRedirectURI,
5
- OAuthRedirectUri,
6
- oauthLoopbackRedirectURISchema,
4
+ oauthLoopbackClientRedirectUriSchema,
7
5
  } from './oauth-redirect-uri.js'
8
6
  import { OAuthScope, oauthScopeSchema } from './oauth-scope.js'
9
7
 
10
- const PREFIX = 'http://localhost'
8
+ export const LOOPBACK_CLIENT_ID_ORIGIN = 'http://localhost'
9
+
10
+ // @NOTE This is not actually based on a standard, but rather a convention
11
+ // established by Bluesky in the Atproto specs and implementation. As such, and
12
+ // in order to respect the convention from this package, these should be
13
+ // prefixed with "Atproto" instead of "OAuth". For legacy reasons, we keep the
14
+ // current names, but we should rename them in a future major release, unless
15
+ // loopback client ids have since then been standardized.
16
+
17
+ export type OAuthClientIdLoopback =
18
+ `http://localhost${'' | `/`}${'' | `?${string}`}`
19
+
20
+ export type OAuthLoopbackClientIdParams = {
21
+ scope?: OAuthScope
22
+ redirect_uris?: [OAuthLoopbackRedirectURI, ...OAuthLoopbackRedirectURI[]]
23
+ }
11
24
 
12
25
  export const oauthClientIdLoopbackSchema = oauthClientIdSchema.superRefine(
13
- (value, ctx): value is `${typeof PREFIX}${'' | '/'}${'' | `?${string}`}` => {
14
- try {
15
- assertOAuthLoopbackClientId(value)
16
- return true
17
- } catch (error) {
18
- ctx.addIssue({
19
- code: ZodIssueCode.custom,
20
- message:
21
- error instanceof TypeError
22
- ? error.message
23
- : 'Invalid loopback client ID',
24
- })
25
- return false
26
+ (input, ctx): input is OAuthClientIdLoopback => {
27
+ const result = safeParseOAuthLoopbackClientId(input)
28
+ if (!result.success) {
29
+ ctx.addIssue({ code: 'custom', message: result.message })
26
30
  }
31
+ return result.success
27
32
  },
28
33
  )
29
34
 
30
- export type OAuthClientIdLoopback = TypeOf<typeof oauthClientIdLoopbackSchema>
35
+ export function assertOAuthLoopbackClientId(
36
+ input: string,
37
+ ): asserts input is OAuthClientIdLoopback {
38
+ void parseOAuthLoopbackClientId(input)
39
+ }
31
40
 
32
- export function isOAuthClientIdLoopback(
33
- clientId: string,
34
- ): clientId is OAuthClientIdLoopback {
35
- try {
36
- parseOAuthLoopbackClientId(clientId)
37
- return true
38
- } catch {
39
- return false
40
- }
41
+ export function isOAuthClientIdLoopback<T extends string>(
42
+ input: T,
43
+ ): input is T & OAuthClientIdLoopback {
44
+ return safeParseOAuthLoopbackClientId(input).success
41
45
  }
42
46
 
43
- export function assertOAuthLoopbackClientId(
44
- clientId: string,
45
- ): asserts clientId is OAuthClientIdLoopback {
46
- void parseOAuthLoopbackClientId(clientId)
47
+ export function asOAuthClientIdLoopback<T extends string>(input: T) {
48
+ assertOAuthLoopbackClientId(input)
49
+ return input
47
50
  }
48
51
 
49
- // @TODO should we turn this into a zod schema? (more coherent error with other
50
- // validation functions)
51
- export function parseOAuthLoopbackClientId(clientId: string): {
52
- scope?: OAuthScope
53
- redirect_uris?: [OAuthRedirectUri, ...OAuthRedirectUri[]]
54
- } {
55
- if (!clientId.startsWith(PREFIX)) {
56
- throw new TypeError(`Loopback ClientID must start with "${PREFIX}"`)
57
- } else if (clientId.includes('#', PREFIX.length)) {
58
- throw new TypeError('Loopback ClientID must not contain a hash component')
59
- }
52
+ export function parseOAuthLoopbackClientId(
53
+ input: string,
54
+ ): OAuthLoopbackClientIdParams {
55
+ const result = safeParseOAuthLoopbackClientId(input)
56
+ if (result.success) return result.value
60
57
 
61
- const queryStringIdx =
62
- clientId.length > PREFIX.length && clientId[PREFIX.length] === '/'
63
- ? PREFIX.length + 1
64
- : PREFIX.length
58
+ throw new TypeError(`Invalid loopback client ID: ${result.message}`)
59
+ }
65
60
 
66
- if (clientId.length === queryStringIdx) {
67
- return {} // no query string to parse
61
+ /**
62
+ * Similar to Zod's {@link SafeParseReturnType} but uses a simple "message"
63
+ * string instead of an "error" Error object.
64
+ */
65
+ type LightParseReturnType<T> =
66
+ | { success: true; value: T }
67
+ | { success: false; message: string }
68
+
69
+ export function safeParseOAuthLoopbackClientId(
70
+ input: string,
71
+ ): LightParseReturnType<OAuthLoopbackClientIdParams> {
72
+ // @NOTE Not using "new URL" to ensure input indeed matches the type
73
+ // OAuthClientIdLoopback
74
+
75
+ if (!input.startsWith(LOOPBACK_CLIENT_ID_ORIGIN)) {
76
+ return {
77
+ success: false,
78
+ message: `Value must start with "${LOOPBACK_CLIENT_ID_ORIGIN}"`,
79
+ }
68
80
  }
69
81
 
70
- if (clientId[queryStringIdx] !== '?') {
71
- throw new TypeError('Loopback ClientID must not contain a path component')
82
+ if (input.includes('#', LOOPBACK_CLIENT_ID_ORIGIN.length)) {
83
+ return {
84
+ success: false,
85
+ message: 'Value must not contain a hash component',
86
+ }
72
87
  }
73
88
 
74
- const searchParams = new URLSearchParams(clientId.slice(queryStringIdx + 1))
89
+ // Since we don't allow a path component (except for a single "/") the query
90
+ // string starts after the origin (+ 1 if there is a "/")
91
+ const queryStringIdx =
92
+ input.length > LOOPBACK_CLIENT_ID_ORIGIN.length &&
93
+ input.charCodeAt(LOOPBACK_CLIENT_ID_ORIGIN.length) === 0x2f /* '/' */
94
+ ? LOOPBACK_CLIENT_ID_ORIGIN.length + 1
95
+ : LOOPBACK_CLIENT_ID_ORIGIN.length
75
96
 
76
- for (const name of searchParams.keys()) {
77
- if (name !== 'redirect_uri' && name !== 'scope') {
78
- throw new TypeError(`Invalid query parameter "${name}" in client ID`)
97
+ // Since we determined the position of the query string based on the origin
98
+ // length (instead of looking for a "?"), we need to make sure the query
99
+ // string position (if any) indeed starts with a "?".
100
+ if (
101
+ input.length !== queryStringIdx &&
102
+ input.charCodeAt(queryStringIdx) !== 0x3f /* '?' */
103
+ ) {
104
+ return {
105
+ success: false,
106
+ message: 'Value must not contain a path component',
79
107
  }
80
108
  }
81
109
 
82
- const scope = searchParams.get('scope') ?? undefined
83
- if (scope != null) {
84
- if (searchParams.getAll('scope').length > 1) {
85
- throw new TypeError(
86
- 'Loopback ClientID must contain at most one scope query parameter',
87
- )
88
- } else if (!oauthScopeSchema.safeParse(scope).success) {
89
- throw new TypeError('Invalid scope query parameter in client ID')
110
+ const queryString = input.slice(queryStringIdx + 1)
111
+ return safeParseOAuthLoopbackClientIdQueryString(queryString)
112
+ }
113
+
114
+ export function safeParseOAuthLoopbackClientIdQueryString(
115
+ input: string | Iterable<[key: string, value: string]>,
116
+ ): LightParseReturnType<OAuthLoopbackClientIdParams> {
117
+ // Parse query params
118
+ const params: OAuthLoopbackClientIdParams = {}
119
+
120
+ const it = typeof input === 'string' ? new URLSearchParams(input) : input
121
+ for (const [key, value] of it) {
122
+ if (key === 'scope') {
123
+ if ('scope' in params) {
124
+ return {
125
+ success: false,
126
+ message: 'Duplicate "scope" query parameter',
127
+ }
128
+ }
129
+
130
+ const res = oauthScopeSchema.safeParse(value)
131
+ if (!res.success) {
132
+ const reason = res.error.issues.map((i) => i.message).join(', ')
133
+ return {
134
+ success: false,
135
+ message: `Invalid "scope" query parameter: ${reason || 'Validation failed'}`,
136
+ }
137
+ }
138
+
139
+ params.scope = res.data
140
+ } else if (key === 'redirect_uri') {
141
+ const res = oauthLoopbackClientRedirectUriSchema.safeParse(value)
142
+ if (!res.success) {
143
+ const reason = res.error.issues.map((i) => i.message).join(', ')
144
+ return {
145
+ success: false,
146
+ message: `Invalid "redirect_uri" query parameter: ${reason || 'Validation failed'}`,
147
+ }
148
+ }
149
+
150
+ if (params.redirect_uris == null) params.redirect_uris = [res.data]
151
+ else params.redirect_uris.push(res.data)
152
+ } else {
153
+ return {
154
+ success: false,
155
+ message: `Unexpected query parameter "${key}"`,
156
+ }
90
157
  }
91
158
  }
92
159
 
93
- const redirect_uris = searchParams.has('redirect_uri')
94
- ? (searchParams
95
- .getAll('redirect_uri')
96
- .map((value) => oauthLoopbackRedirectURISchema.parse(value)) as [
97
- OAuthLoopbackRedirectURI,
98
- ...OAuthLoopbackRedirectURI[],
99
- ])
100
- : undefined
101
-
102
160
  return {
103
- scope,
104
- redirect_uris,
161
+ success: true,
162
+ value: params,
105
163
  }
106
164
  }