@atpassport/client 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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const vitest_1 = require("vitest");
37
+ const index_1 = require("../index");
38
+ const jose_1 = require("jose");
39
+ (0, vitest_1.describe)('AtPassport', () => {
40
+ const baseUrl = 'https://passport.atproto.com';
41
+ const callbackUrl = 'https://app.com/callback';
42
+ let privateKey;
43
+ let publicKey;
44
+ (0, vitest_1.beforeEach)(async () => {
45
+ const keyPair = await (0, jose_1.generateKeyPair)('RS256');
46
+ privateKey = await (0, jose_1.exportPKCS8)(keyPair.privateKey);
47
+ publicKey = await (0, jose_1.exportSPKI)(keyPair.publicKey);
48
+ // Polyfill crypto.randomUUID for vitest environment if needed
49
+ if (typeof crypto === 'undefined' || typeof crypto.randomUUID !== 'function') {
50
+ const g = global;
51
+ if (!g.crypto)
52
+ g.crypto = {};
53
+ g.crypto.randomUUID = () => 'test-uuid-1234';
54
+ }
55
+ });
56
+ (0, vitest_1.it)('generates correct register URL', () => {
57
+ const passport = new index_1.AtPassport({ baseUrl, callbackUrl });
58
+ const url = passport.registerUrl('alice.bsky.social', 'https://app.com/register-callback');
59
+ const parsed = new URL(url);
60
+ (0, vitest_1.expect)(parsed.origin).toBe(baseUrl);
61
+ (0, vitest_1.expect)(parsed.pathname).toBe('/api/register');
62
+ (0, vitest_1.expect)(parsed.searchParams.get('handle')).toBe('alice.bsky.social');
63
+ (0, vitest_1.expect)(parsed.searchParams.get('callbackUrl')).toBe('https://app.com/register-callback');
64
+ });
65
+ (0, vitest_1.it)('generates correct resolve URL', () => {
66
+ const passport = new index_1.AtPassport({ baseUrl, callbackUrl });
67
+ const url = passport.resolveUrl('https://app.com/resolve-callback');
68
+ const parsed = new URL(url);
69
+ (0, vitest_1.expect)(parsed.origin).toBe(baseUrl);
70
+ (0, vitest_1.expect)(parsed.pathname).toBe('/api/resolve');
71
+ (0, vitest_1.expect)(parsed.searchParams.get('callbackUrl')).toBe('https://app.com/resolve-callback');
72
+ });
73
+ (0, vitest_1.it)('gets session from a valid token', async () => {
74
+ const passport = new index_1.AtPassport({ baseUrl, callbackUrl, publicKey });
75
+ const payload = { did: 'did:plc:123', handle: 'alice.bsky.social', uuid: 'uuid-123' };
76
+ const token = await new jose_1.SignJWT(payload)
77
+ .setProtectedHeader({ alg: 'RS256' })
78
+ .setIssuedAt()
79
+ .setExpirationTime('1h')
80
+ .setIssuer('atpassport')
81
+ .sign(await importPKCS8(privateKey, 'RS256'));
82
+ const verified = await passport.get(token);
83
+ (0, vitest_1.expect)(verified.did).toBe(payload.did);
84
+ (0, vitest_1.expect)(verified.handle).toBe(payload.handle);
85
+ });
86
+ (0, vitest_1.it)('generates correct auth URL and state', () => {
87
+ const passport = new index_1.AtPassport({ baseUrl, callbackUrl });
88
+ const { url, atpstate } = passport.generateAuthUrl({ theme: 'dark' });
89
+ const parsed = new URL(url);
90
+ (0, vitest_1.expect)(parsed.origin).toBe(baseUrl);
91
+ (0, vitest_1.expect)(parsed.pathname).toBe('/authentication');
92
+ (0, vitest_1.expect)(parsed.searchParams.get('atpstate')).toBe(atpstate);
93
+ const innerCallback = new URL(parsed.searchParams.get('callback'));
94
+ (0, vitest_1.expect)(innerCallback.origin).toBe('https://app.com');
95
+ (0, vitest_1.expect)(innerCallback.searchParams.get('theme')).toBe('dark');
96
+ });
97
+ (0, vitest_1.it)('parses callback URL correctly', () => {
98
+ const passport = new index_1.AtPassport({ callbackUrl });
99
+ const testUrl = 'https://app.com/callback?handle=alice.bsky.social&did=did:plc:123&pdsurl=https://pds.example.com&atpstate=test-state&extra=param';
100
+ const result = passport.parseCallback(testUrl);
101
+ (0, vitest_1.expect)(result.handle).toBe('alice.bsky.social');
102
+ (0, vitest_1.expect)(result.did).toBe('did:plc:123');
103
+ (0, vitest_1.expect)(result.pdsUrl).toBe('https://pds.example.com');
104
+ (0, vitest_1.expect)(result.atpstate).toBe('test-state');
105
+ (0, vitest_1.expect)(result.customParams.extra).toBe('param');
106
+ (0, vitest_1.expect)(result.customParams.handle).toBeUndefined();
107
+ });
108
+ });
109
+ async function importPKCS8(pkcs8, alg) {
110
+ const { importPKCS8 } = await Promise.resolve().then(() => __importStar(require('jose')));
111
+ return await importPKCS8(pkcs8, alg);
112
+ }
@@ -0,0 +1,60 @@
1
+ export interface AtPassportSession {
2
+ did: string;
3
+ handle: string;
4
+ uuid: string;
5
+ }
6
+ export interface AtPassportOptions {
7
+ callbackUrl: string;
8
+ baseUrl?: string;
9
+ publicKey?: string;
10
+ }
11
+ /**
12
+ * AtPassport Client
13
+ */
14
+ export declare class AtPassport {
15
+ private readonly baseUrl;
16
+ private readonly publicKey?;
17
+ private readonly callbackUrl;
18
+ constructor(options: AtPassportOptions);
19
+ /**
20
+ * Generates the URL for handle registration.
21
+ */
22
+ registerUrl(handle: string, callback: string): string;
23
+ /**
24
+ * Generates the URL for identity resolution.
25
+ */
26
+ resolveUrl(callback: string): string;
27
+ /**
28
+ * Verifies the token and returns the session.
29
+ */
30
+ get(token: string): Promise<AtPassportSession>;
31
+ /**
32
+ * Generates the authentication URL and state.
33
+ * By saving the state on the app side and verifying it in parseCallback,
34
+ * you can prevent CSRF attacks. Custom parameters will be attached to the callback.
35
+ */
36
+ generateAuthUrl(customParams?: Record<string, string>): {
37
+ url: string;
38
+ atpstate: string;
39
+ };
40
+ /**
41
+ * Parses the callback URL returned from AtPassport and extracts parameters.
42
+ */
43
+ parseCallback(currentUrl: string): {
44
+ handle: string | null;
45
+ did: string | null;
46
+ pdsUrl: string | null;
47
+ atpstate: string | null;
48
+ customParams: Record<string, string>;
49
+ };
50
+ /**
51
+ * Opens a popup to let user pick a handle.
52
+ * Returns a promise that resolves with the selected handle.
53
+ */
54
+ pick(): Promise<string>;
55
+ /**
56
+ * Attaches the picker to an input element.
57
+ * Automatically fills the input when a handle is picked on focus.
58
+ */
59
+ decorate(input: HTMLInputElement): void;
60
+ }
package/dist/index.js ADDED
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AtPassport = void 0;
4
+ const jose_1 = require("jose");
5
+ /**
6
+ * AtPassport Client
7
+ */
8
+ class AtPassport {
9
+ baseUrl;
10
+ publicKey;
11
+ callbackUrl;
12
+ constructor(options) {
13
+ this.baseUrl = (options.baseUrl || "https://atpassport.net").replace(/\/$/, "");
14
+ this.publicKey = options.publicKey;
15
+ this.callbackUrl = options.callbackUrl;
16
+ }
17
+ /**
18
+ * Generates the URL for handle registration.
19
+ */
20
+ registerUrl(handle, callback) {
21
+ const url = new URL(`${this.baseUrl}/api/register`);
22
+ url.searchParams.set("handle", handle);
23
+ url.searchParams.set("callbackUrl", callback);
24
+ return url.toString();
25
+ }
26
+ /**
27
+ * Generates the URL for identity resolution.
28
+ */
29
+ resolveUrl(callback) {
30
+ const url = new URL(`${this.baseUrl}/api/resolve`);
31
+ url.searchParams.set("callbackUrl", callback);
32
+ return url.toString();
33
+ }
34
+ /**
35
+ * Verifies the token and returns the session.
36
+ */
37
+ async get(token) {
38
+ if (!this.publicKey) {
39
+ throw new Error("Public key is required for verification");
40
+ }
41
+ const spki = this.publicKey.includes("BEGIN")
42
+ ? this.publicKey
43
+ : Buffer.from(this.publicKey, 'base64').toString();
44
+ const key = await (0, jose_1.importSPKI)(spki, "RS256");
45
+ const { payload } = await (0, jose_1.jwtVerify)(token, key, { issuer: "atpassport" });
46
+ return payload;
47
+ }
48
+ /**
49
+ * Generates the authentication URL and state.
50
+ * By saving the state on the app side and verifying it in parseCallback,
51
+ * you can prevent CSRF attacks. Custom parameters will be attached to the callback.
52
+ */
53
+ generateAuthUrl(customParams) {
54
+ const atpstate = crypto.randomUUID(); // Requires browser context or Node.js 19+
55
+ const url = new URL(`${this.baseUrl}/authentication`);
56
+ const callback = new URL(this.callbackUrl);
57
+ if (customParams) {
58
+ for (const [key, value] of Object.entries(customParams)) {
59
+ callback.searchParams.set(key, value);
60
+ }
61
+ }
62
+ url.searchParams.set("callback", callback.toString());
63
+ url.searchParams.set("atpstate", atpstate);
64
+ return { url: url.toString(), atpstate };
65
+ }
66
+ /**
67
+ * Parses the callback URL returned from AtPassport and extracts parameters.
68
+ */
69
+ parseCallback(currentUrl) {
70
+ const url = new URL(currentUrl);
71
+ const handle = url.searchParams.get("handle");
72
+ const did = url.searchParams.get("did");
73
+ const pdsUrl = url.searchParams.get("pdsurl");
74
+ const atpstate = url.searchParams.get("atpstate");
75
+ const customParams = {};
76
+ url.searchParams.forEach((value, key) => {
77
+ if (!["handle", "did", "pdsurl", "atpstate"].includes(key)) {
78
+ customParams[key] = value;
79
+ }
80
+ });
81
+ return { handle, did, pdsUrl, atpstate, customParams };
82
+ }
83
+ /**
84
+ * Opens a popup to let user pick a handle.
85
+ * Returns a promise that resolves with the selected handle.
86
+ */
87
+ async pick() {
88
+ return new Promise((resolve) => {
89
+ const width = 400;
90
+ const height = 500;
91
+ const left = window.screenX + (window.outerWidth - width) / 2;
92
+ const top = window.screenY + (window.outerHeight - height) / 2;
93
+ const pickerUrl = `${this.baseUrl}/picker`;
94
+ const popup = window.open(pickerUrl, 'atpassport:picker', `width=${width},height=${height},left=${left},top=${top}`);
95
+ const handler = (event) => {
96
+ if (event.origin !== new URL(this.baseUrl).origin)
97
+ return;
98
+ if (event.data?.type === 'atpassport:pick') {
99
+ window.removeEventListener('message', handler);
100
+ resolve(event.data.handle);
101
+ }
102
+ };
103
+ window.addEventListener('message', handler);
104
+ // Optional: Check if popup is closed without picking
105
+ const checkClosed = setInterval(() => {
106
+ if (popup?.closed) {
107
+ clearInterval(checkClosed);
108
+ // resolve empty if closed?
109
+ }
110
+ }, 1000);
111
+ });
112
+ }
113
+ /**
114
+ * Attaches the picker to an input element.
115
+ * Automatically fills the input when a handle is picked on focus.
116
+ */
117
+ decorate(input) {
118
+ input.addEventListener('focus', async () => {
119
+ if (input.value)
120
+ return; // Don't interrupt if already filled
121
+ const handle = await this.pick();
122
+ if (handle) {
123
+ input.value = handle;
124
+ // Trigger change events
125
+ input.dispatchEvent(new Event('input', { bubbles: true }));
126
+ input.dispatchEvent(new Event('change', { bubbles: true }));
127
+ }
128
+ });
129
+ }
130
+ }
131
+ exports.AtPassport = AtPassport;
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@atpassport/client",
3
+ "version": "0.1.0",
4
+ "main": "dist/index.js",
5
+ "types": "dist/index.d.ts",
6
+ "files": [
7
+ "dist",
8
+ "README.md"
9
+ ],
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "test": "vitest run"
16
+ },
17
+ "dependencies": {
18
+ "jose": "^5.0.0"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^25.5.0",
22
+ "typescript": "^5.0.0",
23
+ "vitest": "^2.0.0"
24
+ }
25
+ }