@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.
- package/LICENSE +14 -0
- package/README.md +48 -0
- package/dist/build-client-metadata.d.ts +168 -0
- package/dist/build-client-metadata.d.ts.map +1 -0
- package/dist/build-client-metadata.js +53 -0
- package/dist/build-client-metadata.js.map +1 -0
- package/dist/constants.d.ts +5 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +5 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/schemas/atcute-confidential-client-metadata.d.ts +21 -0
- package/dist/schemas/atcute-confidential-client-metadata.d.ts.map +1 -0
- package/dist/schemas/atcute-confidential-client-metadata.js +112 -0
- package/dist/schemas/atcute-confidential-client-metadata.js.map +1 -0
- package/dist/schemas/atproto-authorization-server-metadata.d.ts +55 -0
- package/dist/schemas/atproto-authorization-server-metadata.d.ts.map +1 -0
- package/dist/schemas/atproto-authorization-server-metadata.js +25 -0
- package/dist/schemas/atproto-authorization-server-metadata.js.map +1 -0
- package/dist/schemas/atproto-oauth-scope.d.ts +8 -0
- package/dist/schemas/atproto-oauth-scope.d.ts.map +1 -0
- package/dist/schemas/atproto-oauth-scope.js +12 -0
- package/dist/schemas/atproto-oauth-scope.js.map +1 -0
- package/dist/schemas/atproto-oauth-token-response.d.ts +19 -0
- package/dist/schemas/atproto-oauth-token-response.d.ts.map +1 -0
- package/dist/schemas/atproto-oauth-token-response.js +16 -0
- package/dist/schemas/atproto-oauth-token-response.js.map +1 -0
- package/dist/schemas/atproto-protected-resource-metadata.d.ts +21 -0
- package/dist/schemas/atproto-protected-resource-metadata.d.ts.map +1 -0
- package/dist/schemas/atproto-protected-resource-metadata.js +18 -0
- package/dist/schemas/atproto-protected-resource-metadata.js.map +1 -0
- package/dist/schemas/jwk.d.ts +241 -0
- package/dist/schemas/jwk.d.ts.map +1 -0
- package/dist/schemas/jwk.js +138 -0
- package/dist/schemas/jwk.js.map +1 -0
- package/dist/schemas/jwks.d.ts +242 -0
- package/dist/schemas/jwks.d.ts.map +1 -0
- package/dist/schemas/jwks.js +34 -0
- package/dist/schemas/jwks.js.map +1 -0
- package/dist/schemas/oauth-authorization-details.d.ts +64 -0
- package/dist/schemas/oauth-authorization-details.d.ts.map +1 -0
- package/dist/schemas/oauth-authorization-details.js +37 -0
- package/dist/schemas/oauth-authorization-details.js.map +1 -0
- package/dist/schemas/oauth-authorization-server-metadata.d.ts +96 -0
- package/dist/schemas/oauth-authorization-server-metadata.d.ts.map +1 -0
- package/dist/schemas/oauth-authorization-server-metadata.js +81 -0
- package/dist/schemas/oauth-authorization-server-metadata.js.map +1 -0
- package/dist/schemas/oauth-client-id-discoverable.d.ts +6 -0
- package/dist/schemas/oauth-client-id-discoverable.d.ts.map +1 -0
- package/dist/schemas/oauth-client-id-discoverable.js +43 -0
- package/dist/schemas/oauth-client-id-discoverable.js.map +1 -0
- package/dist/schemas/oauth-client-id.d.ts +5 -0
- package/dist/schemas/oauth-client-id.d.ts.map +1 -0
- package/dist/schemas/oauth-client-id.js +4 -0
- package/dist/schemas/oauth-client-id.js.map +1 -0
- package/dist/schemas/oauth-client-metadata.d.ts +164 -0
- package/dist/schemas/oauth-client-metadata.d.ts.map +1 -0
- package/dist/schemas/oauth-client-metadata.js +74 -0
- package/dist/schemas/oauth-client-metadata.js.map +1 -0
- package/dist/schemas/oauth-code-challenge-method.d.ts +4 -0
- package/dist/schemas/oauth-code-challenge-method.d.ts.map +1 -0
- package/dist/schemas/oauth-code-challenge-method.js +3 -0
- package/dist/schemas/oauth-code-challenge-method.js.map +1 -0
- package/dist/schemas/oauth-endpoint-auth-method.d.ts +4 -0
- package/dist/schemas/oauth-endpoint-auth-method.d.ts.map +1 -0
- package/dist/schemas/oauth-endpoint-auth-method.js +3 -0
- package/dist/schemas/oauth-endpoint-auth-method.js.map +1 -0
- package/dist/schemas/oauth-grant-type.d.ts +4 -0
- package/dist/schemas/oauth-grant-type.d.ts.map +1 -0
- package/dist/schemas/oauth-grant-type.js +4 -0
- package/dist/schemas/oauth-grant-type.js.map +1 -0
- package/dist/schemas/oauth-issuer-identifier.d.ts +4 -0
- package/dist/schemas/oauth-issuer-identifier.d.ts.map +1 -0
- package/dist/schemas/oauth-issuer-identifier.js +21 -0
- package/dist/schemas/oauth-issuer-identifier.js.map +1 -0
- package/dist/schemas/oauth-par-response.d.ts +7 -0
- package/dist/schemas/oauth-par-response.d.ts.map +1 -0
- package/dist/schemas/oauth-par-response.js +7 -0
- package/dist/schemas/oauth-par-response.js.map +1 -0
- package/dist/schemas/oauth-prompt.d.ts +13 -0
- package/dist/schemas/oauth-prompt.d.ts.map +1 -0
- package/dist/schemas/oauth-prompt.js +12 -0
- package/dist/schemas/oauth-prompt.js.map +1 -0
- package/dist/schemas/oauth-protected-resource-metadata.d.ts +66 -0
- package/dist/schemas/oauth-protected-resource-metadata.d.ts.map +1 -0
- package/dist/schemas/oauth-protected-resource-metadata.js +71 -0
- package/dist/schemas/oauth-protected-resource-metadata.js.map +1 -0
- package/dist/schemas/oauth-redirect-uri.d.ts +20 -0
- package/dist/schemas/oauth-redirect-uri.d.ts.map +1 -0
- package/dist/schemas/oauth-redirect-uri.js +32 -0
- package/dist/schemas/oauth-redirect-uri.js.map +1 -0
- package/dist/schemas/oauth-response-mode.d.ts +4 -0
- package/dist/schemas/oauth-response-mode.d.ts.map +1 -0
- package/dist/schemas/oauth-response-mode.js +3 -0
- package/dist/schemas/oauth-response-mode.js.map +1 -0
- package/dist/schemas/oauth-response-type.d.ts +4 -0
- package/dist/schemas/oauth-response-type.d.ts.map +1 -0
- package/dist/schemas/oauth-response-type.js +8 -0
- package/dist/schemas/oauth-response-type.js.map +1 -0
- package/dist/schemas/oauth-scope.d.ts +12 -0
- package/dist/schemas/oauth-scope.d.ts.map +1 -0
- package/dist/schemas/oauth-scope.js +14 -0
- package/dist/schemas/oauth-scope.js.map +1 -0
- package/dist/schemas/oauth-token-response.d.ts +22 -0
- package/dist/schemas/oauth-token-response.d.ts.map +1 -0
- package/dist/schemas/oauth-token-response.js +19 -0
- package/dist/schemas/oauth-token-response.js.map +1 -0
- package/dist/schemas/oauth-token-type.d.ts +5 -0
- package/dist/schemas/oauth-token-type.d.ts.map +1 -0
- package/dist/schemas/oauth-token-type.js +13 -0
- package/dist/schemas/oauth-token-type.js.map +1 -0
- package/dist/schemas/uri.d.ts +18 -0
- package/dist/schemas/uri.d.ts.map +1 -0
- package/dist/schemas/uri.js +81 -0
- package/dist/schemas/uri.js.map +1 -0
- package/dist/schemas/utils.d.ts +32 -0
- package/dist/schemas/utils.d.ts.map +1 -0
- package/dist/schemas/utils.js +94 -0
- package/dist/schemas/utils.js.map +1 -0
- package/dist/scope.d.ts +84 -0
- package/dist/scope.d.ts.map +1 -0
- package/dist/scope.js +102 -0
- package/dist/scope.js.map +1 -0
- package/lib/build-client-metadata.ts +72 -0
- package/lib/constants.ts +5 -0
- package/lib/index.ts +116 -0
- package/lib/schemas/atcute-confidential-client-metadata.ts +139 -0
- package/lib/schemas/atproto-authorization-server-metadata.ts +32 -0
- package/lib/schemas/atproto-oauth-scope.ts +18 -0
- package/lib/schemas/atproto-oauth-token-response.ts +20 -0
- package/lib/schemas/atproto-protected-resource-metadata.ts +24 -0
- package/lib/schemas/jwk.ts +189 -0
- package/lib/schemas/jwks.ts +45 -0
- package/lib/schemas/oauth-authorization-details.ts +43 -0
- package/lib/schemas/oauth-authorization-server-metadata.ts +101 -0
- package/lib/schemas/oauth-client-id-discoverable.ts +53 -0
- package/lib/schemas/oauth-client-id.ts +6 -0
- package/lib/schemas/oauth-client-metadata.ts +83 -0
- package/lib/schemas/oauth-code-challenge-method.ts +5 -0
- package/lib/schemas/oauth-endpoint-auth-method.ts +13 -0
- package/lib/schemas/oauth-grant-type.ts +13 -0
- package/lib/schemas/oauth-issuer-identifier.ts +30 -0
- package/lib/schemas/oauth-par-response.ts +10 -0
- package/lib/schemas/oauth-prompt.ts +20 -0
- package/lib/schemas/oauth-protected-resource-metadata.ts +89 -0
- package/lib/schemas/oauth-redirect-uri.ts +42 -0
- package/lib/schemas/oauth-response-mode.ts +9 -0
- package/lib/schemas/oauth-response-type.ts +17 -0
- package/lib/schemas/oauth-scope.ts +18 -0
- package/lib/schemas/oauth-token-response.ts +22 -0
- package/lib/schemas/oauth-token-type.ts +15 -0
- package/lib/schemas/uri.ts +100 -0
- package/lib/schemas/utils.ts +113 -0
- package/lib/scope.ts +187 -0
- 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
|
+
}
|