@atcute/oauth-types 0.1.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 (158) hide show
  1. package/LICENSE +14 -0
  2. package/README.md +48 -0
  3. package/dist/build-client-metadata.d.ts +168 -0
  4. package/dist/build-client-metadata.d.ts.map +1 -0
  5. package/dist/build-client-metadata.js +53 -0
  6. package/dist/build-client-metadata.js.map +1 -0
  7. package/dist/constants.d.ts +5 -0
  8. package/dist/constants.d.ts.map +1 -0
  9. package/dist/constants.js +5 -0
  10. package/dist/constants.js.map +1 -0
  11. package/dist/index.d.ts +31 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +37 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/schemas/atcute-confidential-client-metadata.d.ts +21 -0
  16. package/dist/schemas/atcute-confidential-client-metadata.d.ts.map +1 -0
  17. package/dist/schemas/atcute-confidential-client-metadata.js +112 -0
  18. package/dist/schemas/atcute-confidential-client-metadata.js.map +1 -0
  19. package/dist/schemas/atproto-authorization-server-metadata.d.ts +55 -0
  20. package/dist/schemas/atproto-authorization-server-metadata.d.ts.map +1 -0
  21. package/dist/schemas/atproto-authorization-server-metadata.js +25 -0
  22. package/dist/schemas/atproto-authorization-server-metadata.js.map +1 -0
  23. package/dist/schemas/atproto-oauth-scope.d.ts +8 -0
  24. package/dist/schemas/atproto-oauth-scope.d.ts.map +1 -0
  25. package/dist/schemas/atproto-oauth-scope.js +12 -0
  26. package/dist/schemas/atproto-oauth-scope.js.map +1 -0
  27. package/dist/schemas/atproto-oauth-token-response.d.ts +19 -0
  28. package/dist/schemas/atproto-oauth-token-response.d.ts.map +1 -0
  29. package/dist/schemas/atproto-oauth-token-response.js +16 -0
  30. package/dist/schemas/atproto-oauth-token-response.js.map +1 -0
  31. package/dist/schemas/atproto-protected-resource-metadata.d.ts +21 -0
  32. package/dist/schemas/atproto-protected-resource-metadata.d.ts.map +1 -0
  33. package/dist/schemas/atproto-protected-resource-metadata.js +18 -0
  34. package/dist/schemas/atproto-protected-resource-metadata.js.map +1 -0
  35. package/dist/schemas/jwk.d.ts +241 -0
  36. package/dist/schemas/jwk.d.ts.map +1 -0
  37. package/dist/schemas/jwk.js +138 -0
  38. package/dist/schemas/jwk.js.map +1 -0
  39. package/dist/schemas/jwks.d.ts +242 -0
  40. package/dist/schemas/jwks.d.ts.map +1 -0
  41. package/dist/schemas/jwks.js +34 -0
  42. package/dist/schemas/jwks.js.map +1 -0
  43. package/dist/schemas/oauth-authorization-details.d.ts +64 -0
  44. package/dist/schemas/oauth-authorization-details.d.ts.map +1 -0
  45. package/dist/schemas/oauth-authorization-details.js +37 -0
  46. package/dist/schemas/oauth-authorization-details.js.map +1 -0
  47. package/dist/schemas/oauth-authorization-server-metadata.d.ts +96 -0
  48. package/dist/schemas/oauth-authorization-server-metadata.d.ts.map +1 -0
  49. package/dist/schemas/oauth-authorization-server-metadata.js +81 -0
  50. package/dist/schemas/oauth-authorization-server-metadata.js.map +1 -0
  51. package/dist/schemas/oauth-client-id-discoverable.d.ts +6 -0
  52. package/dist/schemas/oauth-client-id-discoverable.d.ts.map +1 -0
  53. package/dist/schemas/oauth-client-id-discoverable.js +43 -0
  54. package/dist/schemas/oauth-client-id-discoverable.js.map +1 -0
  55. package/dist/schemas/oauth-client-id.d.ts +5 -0
  56. package/dist/schemas/oauth-client-id.d.ts.map +1 -0
  57. package/dist/schemas/oauth-client-id.js +4 -0
  58. package/dist/schemas/oauth-client-id.js.map +1 -0
  59. package/dist/schemas/oauth-client-metadata.d.ts +164 -0
  60. package/dist/schemas/oauth-client-metadata.d.ts.map +1 -0
  61. package/dist/schemas/oauth-client-metadata.js +74 -0
  62. package/dist/schemas/oauth-client-metadata.js.map +1 -0
  63. package/dist/schemas/oauth-code-challenge-method.d.ts +4 -0
  64. package/dist/schemas/oauth-code-challenge-method.d.ts.map +1 -0
  65. package/dist/schemas/oauth-code-challenge-method.js +3 -0
  66. package/dist/schemas/oauth-code-challenge-method.js.map +1 -0
  67. package/dist/schemas/oauth-endpoint-auth-method.d.ts +4 -0
  68. package/dist/schemas/oauth-endpoint-auth-method.d.ts.map +1 -0
  69. package/dist/schemas/oauth-endpoint-auth-method.js +3 -0
  70. package/dist/schemas/oauth-endpoint-auth-method.js.map +1 -0
  71. package/dist/schemas/oauth-grant-type.d.ts +4 -0
  72. package/dist/schemas/oauth-grant-type.d.ts.map +1 -0
  73. package/dist/schemas/oauth-grant-type.js +4 -0
  74. package/dist/schemas/oauth-grant-type.js.map +1 -0
  75. package/dist/schemas/oauth-issuer-identifier.d.ts +4 -0
  76. package/dist/schemas/oauth-issuer-identifier.d.ts.map +1 -0
  77. package/dist/schemas/oauth-issuer-identifier.js +21 -0
  78. package/dist/schemas/oauth-issuer-identifier.js.map +1 -0
  79. package/dist/schemas/oauth-par-response.d.ts +7 -0
  80. package/dist/schemas/oauth-par-response.d.ts.map +1 -0
  81. package/dist/schemas/oauth-par-response.js +7 -0
  82. package/dist/schemas/oauth-par-response.js.map +1 -0
  83. package/dist/schemas/oauth-prompt.d.ts +13 -0
  84. package/dist/schemas/oauth-prompt.d.ts.map +1 -0
  85. package/dist/schemas/oauth-prompt.js +12 -0
  86. package/dist/schemas/oauth-prompt.js.map +1 -0
  87. package/dist/schemas/oauth-protected-resource-metadata.d.ts +66 -0
  88. package/dist/schemas/oauth-protected-resource-metadata.d.ts.map +1 -0
  89. package/dist/schemas/oauth-protected-resource-metadata.js +71 -0
  90. package/dist/schemas/oauth-protected-resource-metadata.js.map +1 -0
  91. package/dist/schemas/oauth-redirect-uri.d.ts +20 -0
  92. package/dist/schemas/oauth-redirect-uri.d.ts.map +1 -0
  93. package/dist/schemas/oauth-redirect-uri.js +32 -0
  94. package/dist/schemas/oauth-redirect-uri.js.map +1 -0
  95. package/dist/schemas/oauth-response-mode.d.ts +4 -0
  96. package/dist/schemas/oauth-response-mode.d.ts.map +1 -0
  97. package/dist/schemas/oauth-response-mode.js +3 -0
  98. package/dist/schemas/oauth-response-mode.js.map +1 -0
  99. package/dist/schemas/oauth-response-type.d.ts +4 -0
  100. package/dist/schemas/oauth-response-type.d.ts.map +1 -0
  101. package/dist/schemas/oauth-response-type.js +8 -0
  102. package/dist/schemas/oauth-response-type.js.map +1 -0
  103. package/dist/schemas/oauth-scope.d.ts +12 -0
  104. package/dist/schemas/oauth-scope.d.ts.map +1 -0
  105. package/dist/schemas/oauth-scope.js +14 -0
  106. package/dist/schemas/oauth-scope.js.map +1 -0
  107. package/dist/schemas/oauth-token-response.d.ts +22 -0
  108. package/dist/schemas/oauth-token-response.d.ts.map +1 -0
  109. package/dist/schemas/oauth-token-response.js +19 -0
  110. package/dist/schemas/oauth-token-response.js.map +1 -0
  111. package/dist/schemas/oauth-token-type.d.ts +5 -0
  112. package/dist/schemas/oauth-token-type.d.ts.map +1 -0
  113. package/dist/schemas/oauth-token-type.js +13 -0
  114. package/dist/schemas/oauth-token-type.js.map +1 -0
  115. package/dist/schemas/uri.d.ts +18 -0
  116. package/dist/schemas/uri.d.ts.map +1 -0
  117. package/dist/schemas/uri.js +81 -0
  118. package/dist/schemas/uri.js.map +1 -0
  119. package/dist/schemas/utils.d.ts +32 -0
  120. package/dist/schemas/utils.d.ts.map +1 -0
  121. package/dist/schemas/utils.js +94 -0
  122. package/dist/schemas/utils.js.map +1 -0
  123. package/dist/scope.d.ts +84 -0
  124. package/dist/scope.d.ts.map +1 -0
  125. package/dist/scope.js +102 -0
  126. package/dist/scope.js.map +1 -0
  127. package/lib/build-client-metadata.ts +72 -0
  128. package/lib/constants.ts +5 -0
  129. package/lib/index.ts +116 -0
  130. package/lib/schemas/atcute-confidential-client-metadata.ts +139 -0
  131. package/lib/schemas/atproto-authorization-server-metadata.ts +32 -0
  132. package/lib/schemas/atproto-oauth-scope.ts +18 -0
  133. package/lib/schemas/atproto-oauth-token-response.ts +20 -0
  134. package/lib/schemas/atproto-protected-resource-metadata.ts +24 -0
  135. package/lib/schemas/jwk.ts +189 -0
  136. package/lib/schemas/jwks.ts +45 -0
  137. package/lib/schemas/oauth-authorization-details.ts +43 -0
  138. package/lib/schemas/oauth-authorization-server-metadata.ts +101 -0
  139. package/lib/schemas/oauth-client-id-discoverable.ts +53 -0
  140. package/lib/schemas/oauth-client-id.ts +6 -0
  141. package/lib/schemas/oauth-client-metadata.ts +83 -0
  142. package/lib/schemas/oauth-code-challenge-method.ts +5 -0
  143. package/lib/schemas/oauth-endpoint-auth-method.ts +13 -0
  144. package/lib/schemas/oauth-grant-type.ts +13 -0
  145. package/lib/schemas/oauth-issuer-identifier.ts +30 -0
  146. package/lib/schemas/oauth-par-response.ts +10 -0
  147. package/lib/schemas/oauth-prompt.ts +20 -0
  148. package/lib/schemas/oauth-protected-resource-metadata.ts +89 -0
  149. package/lib/schemas/oauth-redirect-uri.ts +42 -0
  150. package/lib/schemas/oauth-response-mode.ts +9 -0
  151. package/lib/schemas/oauth-response-type.ts +17 -0
  152. package/lib/schemas/oauth-scope.ts +18 -0
  153. package/lib/schemas/oauth-token-response.ts +22 -0
  154. package/lib/schemas/oauth-token-type.ts +15 -0
  155. package/lib/schemas/uri.ts +100 -0
  156. package/lib/schemas/utils.ts +113 -0
  157. package/lib/scope.ts +187 -0
  158. package/package.json +38 -0
@@ -0,0 +1,100 @@
1
+ import * as v from '@badrap/valita';
2
+
3
+ import { isHostnameIP, isLocalHostname, isLoopbackHost } from './utils.js';
4
+
5
+ /**
6
+ * valid, but potentially dangerous URL (`data:`, `file:`, `javascript:`, etc.).
7
+ *
8
+ * any value that matches this schema is safe to parse using `new URL()`.
9
+ */
10
+ export const urlSchema = v.string().chain((input) => {
11
+ if (input.includes(':') && URL.canParse(input)) {
12
+ return v.ok(input);
13
+ }
14
+ return v.err(`must be a valid url`);
15
+ });
16
+
17
+ /** loopback URL (http://localhost, http://127.0.0.1, http://[::1]) */
18
+ export const loopbackUriSchema = urlSchema.chain((input) => {
19
+ if (!input.startsWith('http://')) {
20
+ return v.err(`loopback url must use http: protocol`);
21
+ }
22
+
23
+ const url = new URL(input);
24
+ if (!isLoopbackHost(url.hostname)) {
25
+ return v.err(`loopback url must use localhost, 127.0.0.1, or [::1] as hostname`);
26
+ }
27
+
28
+ return v.ok(input);
29
+ });
30
+
31
+ /** HTTPS URL with additional restrictions */
32
+ export const httpsUriSchema = urlSchema.chain((input) => {
33
+ if (!input.startsWith('https://')) {
34
+ return v.err(`url must use https: protocol`);
35
+ }
36
+
37
+ const url = new URL(input);
38
+
39
+ if (isLoopbackHost(url.hostname)) {
40
+ return v.err(`https url must not use a loopback host`);
41
+ }
42
+
43
+ if (!isHostnameIP(url.hostname)) {
44
+ if (!url.hostname.includes('.')) {
45
+ return v.err(`domain name must contain at least two segments`);
46
+ }
47
+ if (url.hostname.endsWith('.local')) {
48
+ return v.err(`domain name must not end with .local`);
49
+ }
50
+ }
51
+
52
+ return v.ok(input);
53
+ });
54
+
55
+ /** web URL (either loopback http or https) */
56
+ export const webUriSchema = urlSchema.chain((input, options) => {
57
+ if (input.startsWith('http://')) {
58
+ return loopbackUriSchema.try(input, options);
59
+ }
60
+
61
+ if (input.startsWith('https://')) {
62
+ return httpsUriSchema.try(input, options);
63
+ }
64
+
65
+ return v.err(`url must use http: or https: protocol`);
66
+ });
67
+
68
+ /** web URL with a non-local hostname */
69
+ export const nonLocalWebUriSchema = webUriSchema.chain((input) => {
70
+ const url = new URL(input);
71
+ if (isLocalHostname(url.hostname)) {
72
+ return v.err(`hostname is invalid`);
73
+ }
74
+ return v.ok(input);
75
+ });
76
+
77
+ /** private-use URI scheme (e.g., com.example.app:/callback) */
78
+ export const privateUseUriSchema = urlSchema.chain((input) => {
79
+ const dotIdx = input.indexOf('.');
80
+ const colonIdx = input.indexOf(':');
81
+
82
+ if (dotIdx === -1 || colonIdx === -1 || dotIdx > colonIdx) {
83
+ return v.err(`private-use uri scheme must contain a dot in the protocol`);
84
+ }
85
+
86
+ const url = new URL(input);
87
+ const scheme = url.protocol.slice(0, -1);
88
+ const domain = scheme.split('.').reverse().join('.');
89
+
90
+ if (isLocalHostname(domain)) {
91
+ return v.err(`private-use uri scheme must not be a local hostname`);
92
+ }
93
+
94
+ // RFC 8252: private-use URIs must use single slash after scheme
95
+ if (url.href.startsWith(`${url.protocol}//`) || url.username || url.password || url.hostname || url.port) {
96
+ return v.err(`private-use uri must be in the form scheme:/<path>`);
97
+ }
98
+
99
+ return v.ok(input);
100
+ });
@@ -0,0 +1,113 @@
1
+ /**
2
+ * checks if a hostname is a loopback address
3
+ */
4
+ export const isLoopbackHost = (hostname: string): boolean => {
5
+ return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]';
6
+ };
7
+
8
+ /**
9
+ * checks if a hostname is an IP address (IPv4 or IPv6)
10
+ */
11
+ export const isHostnameIP = (hostname: string): boolean => {
12
+ // IPv4
13
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(hostname)) {
14
+ return true;
15
+ }
16
+ // IPv6
17
+ if (hostname.startsWith('[') && hostname.endsWith(']')) {
18
+ return true;
19
+ }
20
+ return false;
21
+ };
22
+
23
+ /**
24
+ * checks if a hostname is a local/reserved hostname
25
+ *
26
+ * returns true for single-segment hostnames and reserved TLDs
27
+ */
28
+ export const isLocalHostname = (hostname: string): boolean => {
29
+ const parts = hostname.split('.');
30
+ if (parts.length < 2) {
31
+ return true;
32
+ }
33
+
34
+ const tld = parts.at(-1)!.toLowerCase();
35
+ return tld === 'test' || tld === 'local' || tld === 'localhost' || tld === 'invalid' || tld === 'example';
36
+ };
37
+
38
+ /**
39
+ * extracts the path from a URL without relying on URL constructor normalization
40
+ *
41
+ * this is needed because the URL constructor normalizes paths (e.g., removes `.` and `..` segments),
42
+ * which can be used to bypass validation checks
43
+ */
44
+ export const extractUrlPath = (url: string): string => {
45
+ const endOfProtocol = url.startsWith('https://') ? 8 : url.startsWith('http://') ? 7 : -1;
46
+ if (endOfProtocol === -1) {
47
+ throw new TypeError(`url must use https: or http: protocol`);
48
+ }
49
+
50
+ const hashIdx = url.indexOf('#', endOfProtocol);
51
+ const questionIdx = url.indexOf('?', endOfProtocol);
52
+
53
+ const queryStrIdx = questionIdx !== -1 && (hashIdx === -1 || questionIdx < hashIdx) ? questionIdx : -1;
54
+
55
+ const pathEnd =
56
+ hashIdx === -1
57
+ ? queryStrIdx === -1
58
+ ? url.length
59
+ : queryStrIdx
60
+ : queryStrIdx === -1
61
+ ? hashIdx
62
+ : Math.min(hashIdx, queryStrIdx);
63
+
64
+ const slashIdx = url.indexOf('/', endOfProtocol);
65
+ const pathStart = slashIdx === -1 || slashIdx > pathEnd ? pathEnd : slashIdx;
66
+
67
+ if (endOfProtocol === pathStart) {
68
+ throw new TypeError(`url must contain a host`);
69
+ }
70
+
71
+ return url.substring(pathStart, pathEnd) || '/';
72
+ };
73
+
74
+ /**
75
+ * checks if an item is the last occurrence in an array (for duplicate detection)
76
+ */
77
+ export const isLastOccurrence = <T>(item: T, index: number, array: readonly T[]): boolean => {
78
+ return array.lastIndexOf(item) === index;
79
+ };
80
+
81
+ /**
82
+ * checks if a space-separated string contains a specific value
83
+ *
84
+ * optimized version of `input.split(' ').includes(value)`
85
+ */
86
+ export const isSpaceSeparatedValue = (value: string, input: string): boolean => {
87
+ const inputLength = input.length;
88
+ const valueLength = value.length;
89
+
90
+ if (inputLength < valueLength) {
91
+ return false;
92
+ }
93
+
94
+ let idx = input.indexOf(value);
95
+ let idxEnd: number;
96
+
97
+ while (idx !== -1) {
98
+ idxEnd = idx + valueLength;
99
+
100
+ if (
101
+ // at beginning or preceded by space
102
+ (idx === 0 || input.charCodeAt(idx - 1) === 32) &&
103
+ // at end or followed by space
104
+ (idxEnd === inputLength || input.charCodeAt(idxEnd) === 32)
105
+ ) {
106
+ return true;
107
+ }
108
+
109
+ idx = input.indexOf(value, idxEnd + 1);
110
+ }
111
+
112
+ return false;
113
+ };
package/lib/scope.ts ADDED
@@ -0,0 +1,187 @@
1
+ import type { AtprotoAudience, Nsid } from '@atcute/lexicons/syntax';
2
+
3
+ /** repo record actions */
4
+ export type RepoAction = 'create' | 'update' | 'delete';
5
+
6
+ /** account attributes */
7
+ export type AccountAttr = 'email' | 'repo' | 'status';
8
+
9
+ /** account actions */
10
+ export type AccountAction = 'read' | 'manage';
11
+
12
+ /** identity attributes */
13
+ export type IdentityAttr = 'handle' | '*';
14
+
15
+ /** collection parameter - NSID or wildcard */
16
+ export type CollectionParam = Nsid | '*';
17
+
18
+ /** lexicon method parameter - NSID or wildcard */
19
+ export type LxmParam = Nsid | '*';
20
+
21
+ /** audience parameter - atproto audience or wildcard */
22
+ export type AudParam = AtprotoAudience | '*';
23
+
24
+ export interface RepoOptions {
25
+ /** collection NSID(s) or '*' for all */
26
+ collection: CollectionParam[];
27
+ /** allowed actions; if omitted, all operations are permitted */
28
+ action?: RepoAction[];
29
+ }
30
+
31
+ /**
32
+ * builds a repo permission scope
33
+ * @param options repo permission options
34
+ * @returns scope string like `repo?collection=app.bsky.feed.post&action=create&action=update`
35
+ */
36
+ export const repo = (options: RepoOptions): string => {
37
+ const { collection, action = [] } = options;
38
+
39
+ const params = new URLSearchParams();
40
+ for (const c of collection) {
41
+ params.append('collection', c);
42
+ }
43
+
44
+ for (const a of action) {
45
+ params.append('action', a);
46
+ }
47
+
48
+ return formatScope('repo', params);
49
+ };
50
+
51
+ export interface RpcOptions {
52
+ /** lexicon method NSID(s) or '*' for all */
53
+ lxm: LxmParam[];
54
+ /** audience */
55
+ aud: AudParam;
56
+ }
57
+
58
+ /**
59
+ * builds an rpc permission scope
60
+ * @param options rpc permission options
61
+ * @returns scope string like `rpc?lxm=app.bsky.feed.getFeed&aud=*`
62
+ */
63
+ export const rpc = (options: RpcOptions): string => {
64
+ const { lxm, aud } = options;
65
+
66
+ const params = new URLSearchParams();
67
+ params.set('aud', aud);
68
+
69
+ for (const l of lxm) {
70
+ params.append('lxm', l);
71
+ }
72
+
73
+ return formatScope('rpc', params);
74
+ };
75
+
76
+ export interface AccountOptions {
77
+ /** account attribute (email, repo, status) */
78
+ attr: AccountAttr;
79
+ /** action (read or manage); defaults to read */
80
+ action?: AccountAction;
81
+ }
82
+
83
+ /**
84
+ * builds an account permission scope
85
+ * @param options account permission options
86
+ * @returns scope string like `account?attr=email` or `account?attr=email&action=manage`
87
+ */
88
+ export const account = (options: AccountOptions): string => {
89
+ const { attr, action } = options;
90
+
91
+ const params = new URLSearchParams();
92
+ params.set('attr', attr);
93
+
94
+ if (action !== undefined) {
95
+ params.set('action', action);
96
+ }
97
+
98
+ return formatScope('account', params);
99
+ };
100
+
101
+ export interface BlobOptions {
102
+ /** MIME type(s) to accept (e.g., 'image/*', '*\/*') */
103
+ accept: string[];
104
+ }
105
+
106
+ /**
107
+ * builds a blob permission scope
108
+ * @param options blob permission options
109
+ * @returns scope string like `blob?accept=image/*`
110
+ */
111
+ export const blob = (options: BlobOptions): string => {
112
+ const { accept } = options;
113
+
114
+ const params = new URLSearchParams();
115
+
116
+ for (const a of accept) {
117
+ params.append('accept', a);
118
+ }
119
+
120
+ return formatScope('blob', params);
121
+ };
122
+
123
+ export interface IdentityOptions {
124
+ /** identity attribute ('handle' or '*') */
125
+ attr: IdentityAttr;
126
+ }
127
+
128
+ /**
129
+ * builds an identity permission scope
130
+ * @param options identity permission options
131
+ * @returns scope string like `identity?attr=handle`
132
+ */
133
+ export const identity = (options: IdentityOptions): string => {
134
+ const params = new URLSearchParams();
135
+ params.set('attr', options.attr);
136
+
137
+ return formatScope('identity', params);
138
+ };
139
+
140
+ export interface IncludeOptions {
141
+ /** lexicon NSID */
142
+ nsid: Nsid;
143
+ /** optional audience override */
144
+ aud?: AtprotoAudience;
145
+ }
146
+
147
+ /**
148
+ * builds an include scope for lexicon-defined permission sets
149
+ * @param options include scope options
150
+ * @returns scope string like `include?nsid=app.bsky.permissions&aud=did:web:bsky.app%23appview`
151
+ */
152
+ export const include = (options: IncludeOptions): string => {
153
+ const { nsid, aud } = options;
154
+
155
+ const params = new URLSearchParams();
156
+ params.set('nsid', nsid);
157
+
158
+ if (aud !== undefined) {
159
+ params.set('aud', aud);
160
+ }
161
+
162
+ return formatScope('include', params);
163
+ };
164
+
165
+ // characters that should remain unencoded in scope strings
166
+ const ALLOWED_CHARS = new Set([':', '/', '+', ',', '@', '%']);
167
+
168
+ // format a scope string matching atproto oauth-scopes format
169
+ const formatScope = (prefix: string, params: URLSearchParams): string => {
170
+ if (params.size === 0) {
171
+ return prefix;
172
+ }
173
+
174
+ return `${prefix}?${normalizeEncoding(params.toString())}`;
175
+ };
176
+
177
+ // normalize URL encoding to match atproto format
178
+ // keeps : / + , @ unencoded, but # must stay as %23
179
+ const normalizeEncoding = (value: string): string => {
180
+ return value.replace(/%[0-9A-F]{2}/gi, (match) => {
181
+ const char = decodeURIComponent(match);
182
+ if (ALLOWED_CHARS.has(char)) {
183
+ return char;
184
+ }
185
+ return match.toUpperCase();
186
+ });
187
+ };
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@atcute/oauth-types",
3
+ "version": "0.1.0",
4
+ "description": "OAuth types and schemas for AT Protocol",
5
+ "license": "0BSD",
6
+ "repository": {
7
+ "url": "https://github.com/mary-ext/atcute",
8
+ "directory": "packages/oauth/types"
9
+ },
10
+ "files": [
11
+ "dist/",
12
+ "lib/",
13
+ "!lib/**/*.bench.ts",
14
+ "!lib/**/*.test.ts"
15
+ ],
16
+ "type": "module",
17
+ "sideEffects": false,
18
+ "exports": {
19
+ ".": "./dist/index.js"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "dependencies": {
25
+ "@badrap/valita": "^0.4.6",
26
+ "@atcute/identity": "^1.1.3",
27
+ "@atcute/oauth-keyset": "^0.1.0",
28
+ "@atcute/lexicons": "^1.2.7"
29
+ },
30
+ "devDependencies": {
31
+ "vitest": "^4.0.16"
32
+ },
33
+ "scripts": {
34
+ "build": "tsgo --project tsconfig.build.json",
35
+ "test": "vitest",
36
+ "prepublish": "rm -rf dist; pnpm run build"
37
+ }
38
+ }