@atpassport/client 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # @atpassport/client
2
+
3
+ [@passport](https://atpassport.net) is an authentication provider tailored for the atproto ecosystem.
4
+ Using this client library, you can easily integrate a @passport-powered "handle input assist feature" into your applications (such as browser extensions or web apps) with an OAuth-like flow.
5
+
6
+ *For the Japanese documentation, please see [README_ja.md](./README_ja.md).*
7
+
8
+ ## Features
9
+ - **Zero dependencies**
10
+ - **OAuth-like secure integration flow** (Built-in CSRF protection via `atpstate`)
11
+ - **Custom parameter passthrough** (Query parameters attached to the callback URL are automatically returned)
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install @atpassport/client
17
+ # or
18
+ pnpm add @atpassport/client
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ ```typescript
24
+ import { AtPassport } from '@atpassport/client';
25
+
26
+ // 1. Initialize the client
27
+ const passport = new AtPassport({
28
+ callbackUrl: 'https://myapp.com/oauth/login' // Required: the URL to redirect back to
29
+ });
30
+
31
+ // 2. (Optional) Generate the authentication URL and redirect
32
+ // You can pass custom parameters (e.g., redirect_uri to return to the original page)
33
+ const { url, atpstate } = passport.generateAuthUrl({
34
+ redirect_uri: '/dashboard'
35
+ });
36
+
37
+ // 3. (Optional) For security, save the generated atpstate to sessionStorage or cookies to prevent CSRF
38
+ sessionStorage.setItem('atpstate', atpstate);
39
+
40
+ // 4. Redirect the user to the @passport handle selection screen
41
+ window.location.assign(url);
42
+ ```
43
+
44
+ ```typescript
45
+ // Receive the parameters on your callback page (https://myapp.com/oauth/callback)
46
+ // (Optional) By passing the saved atpstate as the second argument, parseCallback will automatically
47
+ // throw an Error if the states don't match (preventing CSRF attacks).
48
+ const savedState = sessionStorage.getItem('atpstate');
49
+ const result = passport.parseCallback(window.location.href, savedState);
50
+
51
+ console.log('Login successful:', result);
52
+ console.log('Authenticated User:', result.handle);
53
+ console.log('Custom Parameters:', result.customParams['redirect_uri']); // Outputs '/dashboard'
54
+ ```
55
+
56
+ ---
57
+
58
+ ## Explained: Parameters and Placeholders
59
+
60
+ When @passport redirects back to your `callbackUrl`, the following information will be attached as URL parameters.
61
+
62
+ ### Basic Parameters (Automatically extracted by `parseCallback`)
63
+ - **`handle`**: The authenticated user's Bluesky / atproto handle (e.g., `alice.bsky.social`).
64
+ - **`did`**: The user's Decentralized Identifier (DID). (e.g., `did:plc:xxxxxxxx`. Used to resolve the handle or communicate with the user's PDS.)
65
+ - **`pdsurl`**: The endpoint URL of the user's Personal Data Server (PDS).
66
+ - **`atpstate`**: The state string automatically generated for CSRF protection via `generateAuthUrl()`.
67
+
68
+ ### The `{handle}` and `{did}` Placeholder Replacement Feature
69
+ Depending on the specific API endpoint used for integration or the exact implementation of the integrating client, there is a feature where placing `{handle}` or `{did}` string placeholders inside the `callbackUrl` string will instruct @passport to **dynamically string-replace** them with the actual handle/DID upon completion.
70
+
71
+ **Example:**
72
+ If the `callbackUrl` was `https://myapp.com/login?handle={handle}`, @passport would redirect back to `https://myapp.com/login?handle=alice.bsky.social`.
73
+
74
+ *Note: With the standard flow using `@atpassport/client` (`generateAuthUrl` → `parseCallback`), @passport does not rely on string replacement. Instead, it securely appends parameters like `&handle=...` as standardized URL queries. This allows you to simply and securely receive all information using `parseCallback()` without manually formatting placeholders.*
package/README_ja.md ADDED
@@ -0,0 +1,73 @@
1
+ # @atpassport/client
2
+
3
+ [@passport](https://atpassport.net) は、atproto エコシステム向けの認証プロバイダーです。
4
+ このクライアントライブラリを使うことで、あなたのアプリケーション(ブラウザ拡張機能やWebアプリ)に、@passport を利用した「ハンドル入力のアシスト機能」を簡単に組み込むことができます。
5
+
6
+ *For the English documentation, please see [README.md](./README.md).*
7
+
8
+ ## 特徴
9
+ - 依存関係ゼロ(Zero dependencies)
10
+ - OAuthライクなセキュアな連携フロー(CSRF対策のための `atpstate` 対応)
11
+ - 複数のカスタムパラメータの引き回し(コールバックに付与したパラメータが自動で返却されます)
12
+
13
+ ## インストール
14
+
15
+ ```bash
16
+ npm install @atpassport/client
17
+ # or
18
+ pnpm add @atpassport/client
19
+ ```
20
+
21
+ ## 使い方
22
+
23
+ ```typescript
24
+ import { AtPassport } from '@atpassport/client';
25
+
26
+ // 1. クライアントの初期化
27
+ const passport = new AtPassport({
28
+ callbackUrl: 'https://myapp.com/oauth/login' // 必須: 認証後に戻ってくるURL
29
+ });
30
+
31
+ // 2. (任意)認証URLの生成とリダイレクト
32
+ // カスタムパラメータ(例: ログイン後に元いたページに戻すための redirect_uri など)を指定できます
33
+ const { url, atpstate } = passport.generateAuthUrl({
34
+ redirect_uri: '/dashboard'
35
+ });
36
+
37
+ // 3.(任意)セキュリティのため、発行された atpstate をセッションや sessionStorage 等に保存します
38
+ sessionStorage.setItem('atpstate', atpstate);
39
+
40
+ // 4. @passport のハンドル選択画面へリダイレクト
41
+ window.location.assign(url);
42
+ ```
43
+
44
+ ```typescript
45
+ // コールバック先 (https://myapp.com/oauth/callback) でのパラメータ受け取り
46
+ // (任意)保存していた atpstate を第2引数に渡すことで、状態が不一致(CSRFの疑い)の場合はエラー(例外)が発生します
47
+ const savedState = sessionStorage.getItem('atpstate');
48
+ const result = passport.parseCallback(window.location.href, savedState);
49
+
50
+ console.log('ログイン成功:', result);
51
+ console.log('認証ユーザー:', result.handle);
52
+ console.log('カスタムパラメータ:', result.customParams['redirect_uri']); // '/dashboard' が取れる
53
+ ```
54
+
55
+ ---
56
+
57
+ ## パラメータ・プレースホルダーの解説
58
+
59
+ @passport からコールバック URL にリダイレクトされる際、以下の情報が URL パラメータとして付与されます。
60
+
61
+ ### 基本パラメータ(`parseCallback` で自動取得されるもの)
62
+ - **`handle`**: 認証されたユーザーの Bluesky / atproto ハンドル名(例: `alice.bsky.social`)
63
+ - **`did`**: ユーザーの分散型識別子(DID)。(例: `did:plc:xxxxxxxx`。ハンドルの解決やPDSとの通信に利用します)
64
+ - **`pdsurl`**: ユーザーのデータが保存されている PDS (Personal Data Server) のエンドポイント URL。
65
+ - **`atpstate`**: `generateAuthUrl()` で自動生成された CSRF 防止用のステート文字列。リクエスト元の検証に用います。
66
+
67
+ ### `{handle}` などのプレースホルダー置換機能
68
+ 特定のエンドポイントを利用した連携や、組み込み先のクライアント実装次第では、`callbackUrl` 内に `{handle}` や `{did}` といった文字列(プレースホルダー)を含めておくことで、認証完了時に @passport 側でユーザーの実際のハンドルや DID に**動的に文字列置換**されてリダイレクトされる機能があります。
69
+
70
+ **例:**
71
+ `callbackUrl` を `https://myapp.com/login?handle={handle}` にして @passport へ送ると、完了時に `https://myapp.com/login?handle=alice.bsky.social` として戻ってきます。
72
+
73
+ ※ `@atpassport/client` を使った標準の `generateAuthUrl` → `parseCallback` フローでは、@passport はプレースホルダー置換ではなく、安全に `&handle=...` のように標準的なクエリパラメータとして追記する形を採っているため、意識せずに `parseCallback()` だけで全ての情報を簡単に安全に受け取ることができます。
@@ -1,50 +1,11 @@
1
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
2
  Object.defineProperty(exports, "__esModule", { value: true });
36
3
  const vitest_1 = require("vitest");
37
4
  const index_1 = require("../index");
38
- const jose_1 = require("jose");
39
5
  (0, vitest_1.describe)('AtPassport', () => {
40
6
  const baseUrl = 'https://passport.atproto.com';
41
7
  const callbackUrl = 'https://app.com/callback';
42
- let privateKey;
43
- let publicKey;
44
8
  (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
9
  // Polyfill crypto.randomUUID for vitest environment if needed
49
10
  if (typeof crypto === 'undefined' || typeof crypto.randomUUID !== 'function') {
50
11
  const g = global;
@@ -70,19 +31,6 @@ const jose_1 = require("jose");
70
31
  (0, vitest_1.expect)(parsed.pathname).toBe('/api/resolve');
71
32
  (0, vitest_1.expect)(parsed.searchParams.get('callbackUrl')).toBe('https://app.com/resolve-callback');
72
33
  });
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
34
  (0, vitest_1.it)('generates correct auth URL and state', () => {
87
35
  const passport = new index_1.AtPassport({ baseUrl, callbackUrl });
88
36
  const { url, atpstate } = passport.generateAuthUrl({ theme: 'dark' });
@@ -97,7 +45,7 @@ const jose_1 = require("jose");
97
45
  (0, vitest_1.it)('parses callback URL correctly', () => {
98
46
  const passport = new index_1.AtPassport({ callbackUrl });
99
47
  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);
48
+ const result = passport.parseCallback(testUrl, 'test-state');
101
49
  (0, vitest_1.expect)(result.handle).toBe('alice.bsky.social');
102
50
  (0, vitest_1.expect)(result.did).toBe('did:plc:123');
103
51
  (0, vitest_1.expect)(result.pdsUrl).toBe('https://pds.example.com');
@@ -105,8 +53,9 @@ const jose_1 = require("jose");
105
53
  (0, vitest_1.expect)(result.customParams.extra).toBe('param');
106
54
  (0, vitest_1.expect)(result.customParams.handle).toBeUndefined();
107
55
  });
56
+ (0, vitest_1.it)('throws on CSRF state mismatch', () => {
57
+ const passport = new index_1.AtPassport({ callbackUrl });
58
+ const testUrl = 'https://app.com/callback?handle=alice.bsky.social&atpstate=test-state';
59
+ (0, vitest_1.expect)(() => passport.parseCallback(testUrl, 'wrong-state')).toThrow('Invalid atpstate: CSRF validation failed.');
60
+ });
108
61
  });
109
- async function importPKCS8(pkcs8, alg) {
110
- const { importPKCS8 } = await Promise.resolve().then(() => __importStar(require('jose')));
111
- return await importPKCS8(pkcs8, alg);
112
- }
package/dist/index.d.ts CHANGED
@@ -1,19 +1,12 @@
1
- export interface AtPassportSession {
2
- did: string;
3
- handle: string;
4
- uuid: string;
5
- }
6
1
  export interface AtPassportOptions {
7
2
  callbackUrl: string;
8
3
  baseUrl?: string;
9
- publicKey?: string;
10
4
  }
11
5
  /**
12
6
  * AtPassport Client
13
7
  */
14
8
  export declare class AtPassport {
15
9
  private readonly baseUrl;
16
- private readonly publicKey?;
17
10
  private readonly callbackUrl;
18
11
  constructor(options: AtPassportOptions);
19
12
  /**
@@ -24,10 +17,6 @@ export declare class AtPassport {
24
17
  * Generates the URL for identity resolution.
25
18
  */
26
19
  resolveUrl(callback: string): string;
27
- /**
28
- * Verifies the token and returns the session.
29
- */
30
- get(token: string): Promise<AtPassportSession>;
31
20
  /**
32
21
  * Generates the authentication URL and state.
33
22
  * By saving the state on the app side and verifying it in parseCallback,
@@ -39,8 +28,9 @@ export declare class AtPassport {
39
28
  };
40
29
  /**
41
30
  * Parses the callback URL returned from AtPassport and extracts parameters.
31
+ * If `expectedState` is provided, it throws an error if the returned state does not match.
42
32
  */
43
- parseCallback(currentUrl: string): {
33
+ parseCallback(currentUrl: string, expectedState?: string | null): {
44
34
  handle: string | null;
45
35
  did: string | null;
46
36
  pdsUrl: string | null;
package/dist/index.js CHANGED
@@ -1,17 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.AtPassport = void 0;
4
- const jose_1 = require("jose");
5
4
  /**
6
5
  * AtPassport Client
7
6
  */
8
7
  class AtPassport {
9
8
  baseUrl;
10
- publicKey;
11
9
  callbackUrl;
12
10
  constructor(options) {
13
11
  this.baseUrl = (options.baseUrl || "https://atpassport.net").replace(/\/$/, "");
14
- this.publicKey = options.publicKey;
15
12
  this.callbackUrl = options.callbackUrl;
16
13
  }
17
14
  /**
@@ -31,20 +28,6 @@ class AtPassport {
31
28
  url.searchParams.set("callbackUrl", callback);
32
29
  return url.toString();
33
30
  }
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
31
  /**
49
32
  * Generates the authentication URL and state.
50
33
  * By saving the state on the app side and verifying it in parseCallback,
@@ -65,13 +48,17 @@ class AtPassport {
65
48
  }
66
49
  /**
67
50
  * Parses the callback URL returned from AtPassport and extracts parameters.
51
+ * If `expectedState` is provided, it throws an error if the returned state does not match.
68
52
  */
69
- parseCallback(currentUrl) {
53
+ parseCallback(currentUrl, expectedState) {
70
54
  const url = new URL(currentUrl);
71
55
  const handle = url.searchParams.get("handle");
72
56
  const did = url.searchParams.get("did");
73
57
  const pdsUrl = url.searchParams.get("pdsurl");
74
58
  const atpstate = url.searchParams.get("atpstate");
59
+ if (expectedState && atpstate !== expectedState) {
60
+ throw new Error("Invalid atpstate: CSRF validation failed.");
61
+ }
75
62
  const customParams = {};
76
63
  url.searchParams.forEach((value, key) => {
77
64
  if (!["handle", "did", "pdsurl", "atpstate"].includes(key)) {
package/package.json CHANGED
@@ -1,22 +1,21 @@
1
1
  {
2
2
  "name": "@atpassport/client",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "files": [
7
7
  "dist",
8
- "README.md"
8
+ "README.md",
9
+ "README_ja.md"
9
10
  ],
10
11
  "publishConfig": {
11
12
  "access": "public"
12
13
  },
13
14
  "scripts": {
14
15
  "build": "tsc",
16
+ "deploy": "npm publish --access public",
15
17
  "test": "vitest run"
16
18
  },
17
- "dependencies": {
18
- "jose": "^5.0.0"
19
- },
20
19
  "devDependencies": {
21
20
  "@types/node": "^25.5.0",
22
21
  "typescript": "^5.0.0",