ppussh 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/README.md +192 -0
- package/dist/accounts/index.d.ts +4 -0
- package/dist/accounts/index.d.ts.map +1 -0
- package/dist/accounts/index.js +8 -0
- package/dist/accounts/index.js.map +1 -0
- package/dist/accounts/namespace.d.ts +164 -0
- package/dist/accounts/namespace.d.ts.map +1 -0
- package/dist/accounts/namespace.js +293 -0
- package/dist/accounts/namespace.js.map +1 -0
- package/dist/accounts/types.d.ts +81 -0
- package/dist/accounts/types.d.ts.map +1 -0
- package/dist/accounts/types.js +14 -0
- package/dist/accounts/types.js.map +1 -0
- package/dist/client.d.ts +67 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +81 -0
- package/dist/client.js.map +1 -0
- package/dist/errors.d.ts +81 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +94 -0
- package/dist/errors.js.map +1 -0
- package/dist/http.d.ts +30 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +169 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +58 -0
- package/dist/index.js.map +1 -0
- package/dist/payments/index.d.ts +3 -0
- package/dist/payments/index.d.ts.map +1 -0
- package/dist/payments/index.js +6 -0
- package/dist/payments/index.js.map +1 -0
- package/dist/payments/namespace.d.ts +146 -0
- package/dist/payments/namespace.d.ts.map +1 -0
- package/dist/payments/namespace.js +229 -0
- package/dist/payments/namespace.js.map +1 -0
- package/dist/payments/types.d.ts +98 -0
- package/dist/payments/types.d.ts.map +1 -0
- package/dist/payments/types.js +10 -0
- package/dist/payments/types.js.map +1 -0
- package/dist/webhooks.d.ts +56 -0
- package/dist/webhooks.d.ts.map +1 -0
- package/dist/webhooks.js +67 -0
- package/dist/webhooks.js.map +1 -0
- package/package.json +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# ppussh
|
|
2
|
+
|
|
3
|
+
Official TypeScript/JavaScript SDK for the [PPUSSH](https://ppussh.com) platform —
|
|
4
|
+
Accounts (OIDC / OAuth 2.0) and Payments in a single client.
|
|
5
|
+
|
|
6
|
+
## Requirements
|
|
7
|
+
|
|
8
|
+
- Node.js 18+
|
|
9
|
+
- An Accounts **clientId** and **clientSecret** (obtain from the Accounts admin console)
|
|
10
|
+
- A running instance of the Accounts and Payments services
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install ppussh
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Configuration
|
|
19
|
+
|
|
20
|
+
The SDK requires the base URLs for both services. Set them via environment
|
|
21
|
+
variables (recommended for production) or pass them directly to the constructor.
|
|
22
|
+
|
|
23
|
+
| Environment variable | Purpose |
|
|
24
|
+
| ----------------------- | ------------------------------ |
|
|
25
|
+
| `PPUSSH_ACCOUNTS_URL` | Base URL of the Accounts API |
|
|
26
|
+
| `PPUSSH_PAYMENTS_URL` | Base URL of the Payments API |
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
export PPUSSH_ACCOUNTS_URL="https://accounts.example.com"
|
|
30
|
+
export PPUSSH_PAYMENTS_URL="https://payments.example.com"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quick start
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { PpusshClient } from "ppussh";
|
|
37
|
+
|
|
38
|
+
// URLs are read from PPUSSH_ACCOUNTS_URL / PPUSSH_PAYMENTS_URL env vars,
|
|
39
|
+
// or pass them explicitly:
|
|
40
|
+
const client = new PpusshClient({
|
|
41
|
+
clientId: "your-product-client-id",
|
|
42
|
+
clientSecret: "your-product-client-secret",
|
|
43
|
+
paymentsAdminKey: "your-payments-admin-key", // optional; needed for admin calls
|
|
44
|
+
// accountsUrl: "https://accounts.example.com", // or set PPUSSH_ACCOUNTS_URL
|
|
45
|
+
// paymentsUrl: "https://payments.example.com", // or set PPUSSH_PAYMENTS_URL
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### OIDC callback (Express example)
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import express from "express";
|
|
53
|
+
import { PpusshClient } from "ppussh";
|
|
54
|
+
|
|
55
|
+
const app = express();
|
|
56
|
+
const client = new PpusshClient({ clientId: "...", clientSecret: "..." });
|
|
57
|
+
|
|
58
|
+
const REDIRECT_URI = "https://yourapp.example.com/auth/callback";
|
|
59
|
+
|
|
60
|
+
app.get("/auth/callback", async (req, res) => {
|
|
61
|
+
const { code } = req.query as { code: string };
|
|
62
|
+
const token = await client.accounts.exchangeCode(code, REDIRECT_URI);
|
|
63
|
+
// token.user contains the authenticated user's profile
|
|
64
|
+
res.json({ userId: token.user.id, email: token.user.email });
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Token verification middleware
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { PpusshClient, PpusshAuthError } from "ppussh";
|
|
72
|
+
|
|
73
|
+
const client = new PpusshClient({ clientId: "...", clientSecret: "..." });
|
|
74
|
+
|
|
75
|
+
async function requireAuth(req: Request): Promise<string> {
|
|
76
|
+
const auth = req.headers.get("authorization") ?? "";
|
|
77
|
+
if (!auth.startsWith("Bearer ")) throw new Response(null, { status: 401 });
|
|
78
|
+
const bearer = auth.slice(7);
|
|
79
|
+
try {
|
|
80
|
+
const result = await client.accounts.verifyToken(bearer);
|
|
81
|
+
return result.userId;
|
|
82
|
+
} catch (err) {
|
|
83
|
+
if (err instanceof PpusshAuthError) throw new Response(null, { status: 401 });
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Token refresh
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
// Uses the refresh token stored internally after exchangeCode()
|
|
93
|
+
const newToken = await client.accounts.refresh();
|
|
94
|
+
|
|
95
|
+
// Or pass an explicit refresh token:
|
|
96
|
+
const newToken = await client.accounts.refresh("rt_...");
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Logout
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
await client.accounts.logout(); // uses stored refresh token
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Billing — create a customer and subscription
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { randomUUID } from "crypto";
|
|
109
|
+
|
|
110
|
+
// Create or retrieve a customer record
|
|
111
|
+
const customer = await client.payments.createCustomer(token.user.id, {
|
|
112
|
+
workspaceId: "ws-123", // optional
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// List available plans for a product
|
|
116
|
+
const plans = await client.payments.listPlans("prod-abc");
|
|
117
|
+
|
|
118
|
+
// Subscribe the customer
|
|
119
|
+
const subscription = await client.payments.createSubscription({
|
|
120
|
+
customerId: customer.id,
|
|
121
|
+
paymentProductId: "prod-abc",
|
|
122
|
+
planKey: "pro",
|
|
123
|
+
idempotencyKey: randomUUID(),
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Error handling
|
|
128
|
+
|
|
129
|
+
All exceptions are subclasses of `PpusshError`:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import {
|
|
133
|
+
PpusshError, // base class
|
|
134
|
+
PpusshAuthError, // 401 — invalid or expired token / credentials
|
|
135
|
+
PpusshConsentRequired, // 403 — user hasn't consented to this product's scopes
|
|
136
|
+
PpusshPaymentError, // non-2xx from the Payments service
|
|
137
|
+
PpusshNetworkError, // all retries exhausted / connection failure
|
|
138
|
+
} from "ppussh";
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
const token = await client.accounts.exchangeCode(code, REDIRECT_URI);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
if (err instanceof PpusshConsentRequired) {
|
|
144
|
+
// Redirect the user to the consent flow
|
|
145
|
+
redirectToConsent(err.clientId, err.productName);
|
|
146
|
+
} else if (err instanceof PpusshAuthError) {
|
|
147
|
+
// Invalid code or expired credentials
|
|
148
|
+
} else if (err instanceof PpusshNetworkError) {
|
|
149
|
+
// Retry later
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Retry policy
|
|
155
|
+
|
|
156
|
+
| Condition | Behaviour |
|
|
157
|
+
| ----------------------- | -------------------------------------------------------- |
|
|
158
|
+
| 5xx / network error | Up to 3 attempts, exponential backoff (0.5 s, 1 s, 2 s) |
|
|
159
|
+
| 429 Too Many Requests | Respects `Retry-After` header, max 2 retries |
|
|
160
|
+
| 4xx (not 429) | Never retried — raises immediately |
|
|
161
|
+
|
|
162
|
+
## API reference
|
|
163
|
+
|
|
164
|
+
### `client.accounts`
|
|
165
|
+
|
|
166
|
+
| Method | Description |
|
|
167
|
+
| ------ | ----------- |
|
|
168
|
+
| `exchangeCode(code, redirectUri)` | Exchange an auth code for tokens (OIDC callback) |
|
|
169
|
+
| `refresh(refreshToken?)` | Refresh the access token |
|
|
170
|
+
| `verifyToken(accessToken)` | Validate an incoming bearer token (use in middleware) |
|
|
171
|
+
| `logout(refreshToken?)` | Revoke the session |
|
|
172
|
+
| `getUser(accessToken?)` | Fetch the authenticated user's profile |
|
|
173
|
+
| `getEntitlements(accessToken?)` | List the user's product entitlements |
|
|
174
|
+
| `getSessions(accessToken?)` | List the user's active sessions |
|
|
175
|
+
|
|
176
|
+
### `client.payments`
|
|
177
|
+
|
|
178
|
+
| Method | Description |
|
|
179
|
+
| ------ | ----------- |
|
|
180
|
+
| `createCustomer(ownerUserId, opts?)` | Create or retrieve a customer record |
|
|
181
|
+
| `getCustomer(customerId)` | Fetch a customer by ID |
|
|
182
|
+
| `createSubscription(opts)` | Create a subscription |
|
|
183
|
+
| `listSubscriptions(customerId, opts?)` | List subscriptions for a customer |
|
|
184
|
+
| `getSubscription(subscriptionId)` | Fetch a subscription by ID |
|
|
185
|
+
| `cancelSubscription(subscriptionId, opts?)` | Cancel a subscription |
|
|
186
|
+
| `listPlans(paymentProductId)` | List billing plans *(requires `paymentsAdminKey`)* |
|
|
187
|
+
| `getProductByAccountsId(accountsProductId)` | Resolve a payments product by its Accounts ID *(requires `paymentsAdminKey`)* |
|
|
188
|
+
| `getMrr(opts?)` | Fetch MRR analytics *(requires `paymentsAdminKey`)* |
|
|
189
|
+
|
|
190
|
+
## License
|
|
191
|
+
|
|
192
|
+
MIT
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type { EntitlementResponse, LogoutResult, SessionResponse, TokenResponse, UserInToken, UserProfile, VerifyTokenResult, } from "./types";
|
|
2
|
+
export { effectiveAccessToken } from "./types";
|
|
3
|
+
export { AccountsNamespace } from "./namespace";
|
|
4
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/accounts/index.ts"],"names":[],"mappings":"AACA,YAAY,EACV,mBAAmB,EACnB,YAAY,EACZ,eAAe,EACf,aAAa,EACb,WAAW,EACX,WAAW,EACX,iBAAiB,GAClB,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AccountsNamespace = exports.effectiveAccessToken = void 0;
|
|
4
|
+
var types_1 = require("./types");
|
|
5
|
+
Object.defineProperty(exports, "effectiveAccessToken", { enumerable: true, get: function () { return types_1.effectiveAccessToken; } });
|
|
6
|
+
var namespace_1 = require("./namespace");
|
|
7
|
+
Object.defineProperty(exports, "AccountsNamespace", { enumerable: true, get: function () { return namespace_1.AccountsNamespace; } });
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/accounts/index.ts"],"names":[],"mappings":";;;AAUA,iCAA+C;AAAtC,6GAAA,oBAAoB,OAAA;AAC7B,yCAAgD;AAAvC,8GAAA,iBAAiB,OAAA"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AccountsNamespace — server-side OIDC + user operations.
|
|
3
|
+
*
|
|
4
|
+
* Handles the product-backend half of the OIDC flow:
|
|
5
|
+
* buildLoginUrl() → build the redirect URL to send the user to Accounts (synchronous)
|
|
6
|
+
* exchangeCode() → trade the auth code (from callback URL) for tokens
|
|
7
|
+
* refresh() → rotate tokens using a refresh token
|
|
8
|
+
* verifyToken() → validate an incoming access token (e.g. from a request header)
|
|
9
|
+
* logout() → revoke a session via refresh token (POST /oauth/logout)
|
|
10
|
+
* logoutAll() → revoke ALL sessions via access token (POST /auth/logout)
|
|
11
|
+
* revokeSession() → revoke a single session by ID (DELETE /auth/sessions/{id})
|
|
12
|
+
* getUser() → fetch the full user profile for the stored access token
|
|
13
|
+
* getEntitlements() → list entitlements for the authenticated user
|
|
14
|
+
* getSessions() → list active sessions for the authenticated user
|
|
15
|
+
*
|
|
16
|
+
* Session state:
|
|
17
|
+
* After a successful exchangeCode() or refresh() call, the client stores:
|
|
18
|
+
* _accessToken — attached automatically to getUser() / getEntitlements() / getSessions()
|
|
19
|
+
* _refreshToken — used automatically by refresh() and logout() if not passed explicitly
|
|
20
|
+
* _tokenExpiresAt — informational; not used for auto-refresh (caller's responsibility)
|
|
21
|
+
*/
|
|
22
|
+
import { HttpTransport } from "../http";
|
|
23
|
+
import { EntitlementResponse, LogoutResult, SessionResponse, TokenResponse, UserProfile, VerifyTokenResult } from "./types";
|
|
24
|
+
export declare class AccountsNamespace {
|
|
25
|
+
private readonly _http;
|
|
26
|
+
private readonly _clientId;
|
|
27
|
+
private readonly _clientSecret;
|
|
28
|
+
private readonly _accountsUrl;
|
|
29
|
+
private _accessToken;
|
|
30
|
+
private _refreshToken;
|
|
31
|
+
private _tokenExpiresAt;
|
|
32
|
+
constructor(transport: HttpTransport, options: {
|
|
33
|
+
clientId: string;
|
|
34
|
+
clientSecret: string;
|
|
35
|
+
accountsUrl: string;
|
|
36
|
+
});
|
|
37
|
+
/**
|
|
38
|
+
* Build the URL to redirect the user's browser to the Accounts login page.
|
|
39
|
+
*
|
|
40
|
+
* This is step 2 of the OIDC flow — call this in your route handler and
|
|
41
|
+
* issue a 302 redirect to the returned URL. The Accounts frontend handles
|
|
42
|
+
* email/password login as well as Google and GitHub social login; the
|
|
43
|
+
* product backend never needs to call social-auth endpoints directly.
|
|
44
|
+
*
|
|
45
|
+
* @param redirectUri Must exactly match the redirect_uri registered for your product.
|
|
46
|
+
* @param state A cryptographically random string stored in the user's session
|
|
47
|
+
* to prevent CSRF attacks.
|
|
48
|
+
* @param opts.nextUrl Optional URL the Accounts frontend redirects to after login
|
|
49
|
+
* within its own domain (rarely needed).
|
|
50
|
+
* @returns The full login URL, e.g. `https://accounts.example.com/login?client_id=...`
|
|
51
|
+
*/
|
|
52
|
+
buildLoginUrl(redirectUri: string, state: string, opts?: {
|
|
53
|
+
nextUrl?: string;
|
|
54
|
+
}): string;
|
|
55
|
+
/**
|
|
56
|
+
* Exchange the authorization code received on your callback URL for tokens.
|
|
57
|
+
*
|
|
58
|
+
* This is step 6 of the OIDC flow — called by your server after the
|
|
59
|
+
* Accounts frontend redirects the user back to your redirectUri with
|
|
60
|
+
* `?code=...&state=...` in the query string.
|
|
61
|
+
*
|
|
62
|
+
* @param code The raw 64-char hex auth code from the callback URL.
|
|
63
|
+
* @param redirectUri Must exactly match the redirect_uri registered for your product.
|
|
64
|
+
* @returns TokenResponse — contains tokens and an embedded UserInToken.
|
|
65
|
+
* Tokens are also stored internally for subsequent calls.
|
|
66
|
+
* @throws PpusshAuthError If the code is invalid, expired, or already used.
|
|
67
|
+
* @throws PpusshConsentRequired If the user has not consented to your product.
|
|
68
|
+
* @throws PpusshNetworkError If the request fails after all retries.
|
|
69
|
+
*/
|
|
70
|
+
exchangeCode(code: string, redirectUri: string): Promise<TokenResponse>;
|
|
71
|
+
/**
|
|
72
|
+
* Rotate tokens using a refresh token.
|
|
73
|
+
*
|
|
74
|
+
* If refreshToken is omitted, the internally stored refresh token
|
|
75
|
+
* from the last exchangeCode() / refresh() call is used.
|
|
76
|
+
*
|
|
77
|
+
* @throws PpusshAuthError If the refresh token is invalid, expired, or replayed.
|
|
78
|
+
* Note: a replayed token causes ALL sessions to be revoked
|
|
79
|
+
* server-side — this is a security feature, not a bug.
|
|
80
|
+
*/
|
|
81
|
+
refresh(refreshToken?: string): Promise<TokenResponse>;
|
|
82
|
+
/**
|
|
83
|
+
* Validate an access token your server received from an end-user request.
|
|
84
|
+
*
|
|
85
|
+
* Use this in your middleware / request handler to verify that the Bearer
|
|
86
|
+
* token a user sent to your product's API is valid and not expired.
|
|
87
|
+
*
|
|
88
|
+
* @param accessToken The raw JWT string from the `Authorization: Bearer ...` header.
|
|
89
|
+
* @returns VerifyTokenResult with valid, type, user_id, and email.
|
|
90
|
+
* @throws PpusshAuthError If the token is invalid, expired, or the account is deleted.
|
|
91
|
+
*/
|
|
92
|
+
verifyToken(accessToken: string): Promise<VerifyTokenResult>;
|
|
93
|
+
/**
|
|
94
|
+
* Revoke a session and trigger front-channel logout to all connected products.
|
|
95
|
+
*
|
|
96
|
+
* Uses POST /oauth/logout with the refresh token — this is the standard
|
|
97
|
+
* per-session logout that also notifies downstream products via webhooks.
|
|
98
|
+
*
|
|
99
|
+
* If refreshToken is omitted, the internally stored refresh token is used.
|
|
100
|
+
* On success, stored tokens are cleared from the client instance.
|
|
101
|
+
*
|
|
102
|
+
* Logout is always safe to call — if the token is already invalid or the session
|
|
103
|
+
* doesn't exist, the Accounts server returns ok=true silently.
|
|
104
|
+
*
|
|
105
|
+
* @throws PpusshAuthError If client_id or client_secret are invalid.
|
|
106
|
+
*/
|
|
107
|
+
logout(refreshToken?: string): Promise<LogoutResult>;
|
|
108
|
+
/**
|
|
109
|
+
* Revoke **all** sessions for the current user immediately.
|
|
110
|
+
*
|
|
111
|
+
* Uses POST /auth/logout with the access token (Bearer header).
|
|
112
|
+
* Unlike logout(), this does not require a refresh token and revokes every
|
|
113
|
+
* active session across all devices — useful for "sign out everywhere" UX.
|
|
114
|
+
*
|
|
115
|
+
* On success, stored tokens are cleared from the client instance.
|
|
116
|
+
*
|
|
117
|
+
* @param accessToken JWT access token. Optional if stored internally.
|
|
118
|
+
* @throws PpusshAuthError If the token is invalid or expired.
|
|
119
|
+
*/
|
|
120
|
+
logoutAll(accessToken?: string): Promise<void>;
|
|
121
|
+
/**
|
|
122
|
+
* Revoke a specific session by its ID.
|
|
123
|
+
*
|
|
124
|
+
* Uses DELETE /auth/sessions/{sessionId} — the user can only revoke their
|
|
125
|
+
* own sessions. Useful for "sign out of this device" UX in a session
|
|
126
|
+
* management screen.
|
|
127
|
+
*
|
|
128
|
+
* @param sessionId The UUID of the session to revoke (from getSessions()).
|
|
129
|
+
* @param accessToken JWT access token. Optional if stored internally.
|
|
130
|
+
* @throws PpusshAuthError If the token is invalid or the session does not
|
|
131
|
+
* belong to the authenticated user.
|
|
132
|
+
*/
|
|
133
|
+
revokeSession(sessionId: string, accessToken?: string): Promise<void>;
|
|
134
|
+
/**
|
|
135
|
+
* Fetch the full user profile for an access token.
|
|
136
|
+
*
|
|
137
|
+
* If accessToken is omitted, the internally stored token from the last
|
|
138
|
+
* exchangeCode() or refresh() is used.
|
|
139
|
+
*
|
|
140
|
+
* @throws PpusshAuthError If the token is invalid or expired.
|
|
141
|
+
*/
|
|
142
|
+
getUser(accessToken?: string): Promise<UserProfile>;
|
|
143
|
+
/**
|
|
144
|
+
* List products the user has granted consent to (their entitlements).
|
|
145
|
+
*
|
|
146
|
+
* @param accessToken JWT access token. Optional if stored internally.
|
|
147
|
+
*/
|
|
148
|
+
getEntitlements(accessToken?: string): Promise<EntitlementResponse[]>;
|
|
149
|
+
/**
|
|
150
|
+
* List all active sessions for the authenticated user.
|
|
151
|
+
*
|
|
152
|
+
* @param accessToken JWT access token. Optional if stored internally.
|
|
153
|
+
*/
|
|
154
|
+
getSessions(accessToken?: string): Promise<SessionResponse[]>;
|
|
155
|
+
private _storeTokens;
|
|
156
|
+
private _clearTokens;
|
|
157
|
+
/** The currently stored access token, if any. */
|
|
158
|
+
get accessToken(): string | null;
|
|
159
|
+
/** The currently stored refresh token, if any. */
|
|
160
|
+
get refreshToken(): string | null;
|
|
161
|
+
/** UTC Date at which the stored access token expires, if known. */
|
|
162
|
+
get tokenExpiresAt(): Date | null;
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=namespace.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"namespace.d.ts","sourceRoot":"","sources":["../../src/accounts/namespace.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACxC,OAAO,EAEL,mBAAmB,EACnB,YAAY,EACZ,eAAe,EACf,aAAa,EACb,WAAW,EACX,iBAAiB,EAClB,MAAM,SAAS,CAAC;AAEjB,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgB;IACtC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IAEtC,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,aAAa,CAAuB;IAC5C,OAAO,CAAC,eAAe,CAAqB;gBAG1C,SAAS,EAAE,aAAa,EACxB,OAAO,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE;IAU1E;;;;;;;;;;;;;;OAcG;IACH,aAAa,CACX,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAC1B,MAAM;IAcT;;;;;;;;;;;;;;OAcG;IACG,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAe7E;;;;;;;;;OASG;IACG,OAAO,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAuB5D;;;;;;;;;OASG;IACG,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC;IASlE;;;;;;;;;;;;;OAaG;IACG,MAAM,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAoB1D;;;;;;;;;;;OAWG;IACG,SAAS,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBpD;;;;;;;;;;;OAWG;IACG,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAe3E;;;;;;;OAOG;IACG,OAAO,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAgBzD;;;;OAIG;IACG,eAAe,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,EAAE,CAAC;IAW3E;;;;OAIG;IACG,WAAW,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IAanE,OAAO,CAAC,YAAY;IAMpB,OAAO,CAAC,YAAY;IAMpB,iDAAiD;IACjD,IAAI,WAAW,IAAI,MAAM,GAAG,IAAI,CAE/B;IAED,kDAAkD;IAClD,IAAI,YAAY,IAAI,MAAM,GAAG,IAAI,CAEhC;IAED,mEAAmE;IACnE,IAAI,cAAc,IAAI,IAAI,GAAG,IAAI,CAEhC;CACF"}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ppussh/src/accounts/namespace.ts
|
|
3
|
+
/**
|
|
4
|
+
* AccountsNamespace — server-side OIDC + user operations.
|
|
5
|
+
*
|
|
6
|
+
* Handles the product-backend half of the OIDC flow:
|
|
7
|
+
* buildLoginUrl() → build the redirect URL to send the user to Accounts (synchronous)
|
|
8
|
+
* exchangeCode() → trade the auth code (from callback URL) for tokens
|
|
9
|
+
* refresh() → rotate tokens using a refresh token
|
|
10
|
+
* verifyToken() → validate an incoming access token (e.g. from a request header)
|
|
11
|
+
* logout() → revoke a session via refresh token (POST /oauth/logout)
|
|
12
|
+
* logoutAll() → revoke ALL sessions via access token (POST /auth/logout)
|
|
13
|
+
* revokeSession() → revoke a single session by ID (DELETE /auth/sessions/{id})
|
|
14
|
+
* getUser() → fetch the full user profile for the stored access token
|
|
15
|
+
* getEntitlements() → list entitlements for the authenticated user
|
|
16
|
+
* getSessions() → list active sessions for the authenticated user
|
|
17
|
+
*
|
|
18
|
+
* Session state:
|
|
19
|
+
* After a successful exchangeCode() or refresh() call, the client stores:
|
|
20
|
+
* _accessToken — attached automatically to getUser() / getEntitlements() / getSessions()
|
|
21
|
+
* _refreshToken — used automatically by refresh() and logout() if not passed explicitly
|
|
22
|
+
* _tokenExpiresAt — informational; not used for auto-refresh (caller's responsibility)
|
|
23
|
+
*/
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.AccountsNamespace = void 0;
|
|
26
|
+
const types_1 = require("./types");
|
|
27
|
+
class AccountsNamespace {
|
|
28
|
+
constructor(transport, options) {
|
|
29
|
+
this._accessToken = null;
|
|
30
|
+
this._refreshToken = null;
|
|
31
|
+
this._tokenExpiresAt = null;
|
|
32
|
+
this._http = transport;
|
|
33
|
+
this._clientId = options.clientId;
|
|
34
|
+
this._clientSecret = options.clientSecret;
|
|
35
|
+
this._accountsUrl = options.accountsUrl;
|
|
36
|
+
}
|
|
37
|
+
// ── Login URL builder ──────────────────────────────────────────────────────
|
|
38
|
+
/**
|
|
39
|
+
* Build the URL to redirect the user's browser to the Accounts login page.
|
|
40
|
+
*
|
|
41
|
+
* This is step 2 of the OIDC flow — call this in your route handler and
|
|
42
|
+
* issue a 302 redirect to the returned URL. The Accounts frontend handles
|
|
43
|
+
* email/password login as well as Google and GitHub social login; the
|
|
44
|
+
* product backend never needs to call social-auth endpoints directly.
|
|
45
|
+
*
|
|
46
|
+
* @param redirectUri Must exactly match the redirect_uri registered for your product.
|
|
47
|
+
* @param state A cryptographically random string stored in the user's session
|
|
48
|
+
* to prevent CSRF attacks.
|
|
49
|
+
* @param opts.nextUrl Optional URL the Accounts frontend redirects to after login
|
|
50
|
+
* within its own domain (rarely needed).
|
|
51
|
+
* @returns The full login URL, e.g. `https://accounts.example.com/login?client_id=...`
|
|
52
|
+
*/
|
|
53
|
+
buildLoginUrl(redirectUri, state, opts) {
|
|
54
|
+
const params = new URLSearchParams({
|
|
55
|
+
client_id: this._clientId,
|
|
56
|
+
redirect_uri: redirectUri,
|
|
57
|
+
state,
|
|
58
|
+
});
|
|
59
|
+
if (opts?.nextUrl) {
|
|
60
|
+
params.set("next", opts.nextUrl);
|
|
61
|
+
}
|
|
62
|
+
return `${this._accountsUrl}/login?${params.toString()}`;
|
|
63
|
+
}
|
|
64
|
+
// ── OIDC token exchange ────────────────────────────────────────────────────
|
|
65
|
+
/**
|
|
66
|
+
* Exchange the authorization code received on your callback URL for tokens.
|
|
67
|
+
*
|
|
68
|
+
* This is step 6 of the OIDC flow — called by your server after the
|
|
69
|
+
* Accounts frontend redirects the user back to your redirectUri with
|
|
70
|
+
* `?code=...&state=...` in the query string.
|
|
71
|
+
*
|
|
72
|
+
* @param code The raw 64-char hex auth code from the callback URL.
|
|
73
|
+
* @param redirectUri Must exactly match the redirect_uri registered for your product.
|
|
74
|
+
* @returns TokenResponse — contains tokens and an embedded UserInToken.
|
|
75
|
+
* Tokens are also stored internally for subsequent calls.
|
|
76
|
+
* @throws PpusshAuthError If the code is invalid, expired, or already used.
|
|
77
|
+
* @throws PpusshConsentRequired If the user has not consented to your product.
|
|
78
|
+
* @throws PpusshNetworkError If the request fails after all retries.
|
|
79
|
+
*/
|
|
80
|
+
async exchangeCode(code, redirectUri) {
|
|
81
|
+
const response = await this._http.request("POST", "/oauth/token", {
|
|
82
|
+
form: {
|
|
83
|
+
grant_type: "authorization_code",
|
|
84
|
+
code,
|
|
85
|
+
client_id: this._clientId,
|
|
86
|
+
client_secret: this._clientSecret,
|
|
87
|
+
redirect_uri: redirectUri,
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
const token = response.data;
|
|
91
|
+
this._storeTokens(token);
|
|
92
|
+
return token;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Rotate tokens using a refresh token.
|
|
96
|
+
*
|
|
97
|
+
* If refreshToken is omitted, the internally stored refresh token
|
|
98
|
+
* from the last exchangeCode() / refresh() call is used.
|
|
99
|
+
*
|
|
100
|
+
* @throws PpusshAuthError If the refresh token is invalid, expired, or replayed.
|
|
101
|
+
* Note: a replayed token causes ALL sessions to be revoked
|
|
102
|
+
* server-side — this is a security feature, not a bug.
|
|
103
|
+
*/
|
|
104
|
+
async refresh(refreshToken) {
|
|
105
|
+
const tokenToUse = refreshToken ?? this._refreshToken;
|
|
106
|
+
if (!tokenToUse) {
|
|
107
|
+
throw new Error("No refreshToken provided and none stored. " +
|
|
108
|
+
"Call exchangeCode() first or pass refreshToken explicitly.");
|
|
109
|
+
}
|
|
110
|
+
const response = await this._http.request("POST", "/oauth/token", {
|
|
111
|
+
form: {
|
|
112
|
+
grant_type: "refresh_token",
|
|
113
|
+
refresh_token: tokenToUse,
|
|
114
|
+
client_id: this._clientId,
|
|
115
|
+
client_secret: this._clientSecret,
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
const token = response.data;
|
|
119
|
+
this._storeTokens(token);
|
|
120
|
+
return token;
|
|
121
|
+
}
|
|
122
|
+
// ── Token verification ─────────────────────────────────────────────────────
|
|
123
|
+
/**
|
|
124
|
+
* Validate an access token your server received from an end-user request.
|
|
125
|
+
*
|
|
126
|
+
* Use this in your middleware / request handler to verify that the Bearer
|
|
127
|
+
* token a user sent to your product's API is valid and not expired.
|
|
128
|
+
*
|
|
129
|
+
* @param accessToken The raw JWT string from the `Authorization: Bearer ...` header.
|
|
130
|
+
* @returns VerifyTokenResult with valid, type, user_id, and email.
|
|
131
|
+
* @throws PpusshAuthError If the token is invalid, expired, or the account is deleted.
|
|
132
|
+
*/
|
|
133
|
+
async verifyToken(accessToken) {
|
|
134
|
+
const response = await this._http.request("GET", "/auth/verify-token", {
|
|
135
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
136
|
+
});
|
|
137
|
+
return response.data;
|
|
138
|
+
}
|
|
139
|
+
// ── Logout ─────────────────────────────────────────────────────────────────
|
|
140
|
+
/**
|
|
141
|
+
* Revoke a session and trigger front-channel logout to all connected products.
|
|
142
|
+
*
|
|
143
|
+
* Uses POST /oauth/logout with the refresh token — this is the standard
|
|
144
|
+
* per-session logout that also notifies downstream products via webhooks.
|
|
145
|
+
*
|
|
146
|
+
* If refreshToken is omitted, the internally stored refresh token is used.
|
|
147
|
+
* On success, stored tokens are cleared from the client instance.
|
|
148
|
+
*
|
|
149
|
+
* Logout is always safe to call — if the token is already invalid or the session
|
|
150
|
+
* doesn't exist, the Accounts server returns ok=true silently.
|
|
151
|
+
*
|
|
152
|
+
* @throws PpusshAuthError If client_id or client_secret are invalid.
|
|
153
|
+
*/
|
|
154
|
+
async logout(refreshToken) {
|
|
155
|
+
const tokenToUse = refreshToken ?? this._refreshToken;
|
|
156
|
+
if (!tokenToUse) {
|
|
157
|
+
throw new Error("No refreshToken provided and none stored. " +
|
|
158
|
+
"Call exchangeCode() first or pass refreshToken explicitly.");
|
|
159
|
+
}
|
|
160
|
+
const response = await this._http.request("POST", "/oauth/logout", {
|
|
161
|
+
json: {
|
|
162
|
+
refresh_token: tokenToUse,
|
|
163
|
+
client_id: this._clientId,
|
|
164
|
+
client_secret: this._clientSecret,
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
const result = response.data;
|
|
168
|
+
this._clearTokens();
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Revoke **all** sessions for the current user immediately.
|
|
173
|
+
*
|
|
174
|
+
* Uses POST /auth/logout with the access token (Bearer header).
|
|
175
|
+
* Unlike logout(), this does not require a refresh token and revokes every
|
|
176
|
+
* active session across all devices — useful for "sign out everywhere" UX.
|
|
177
|
+
*
|
|
178
|
+
* On success, stored tokens are cleared from the client instance.
|
|
179
|
+
*
|
|
180
|
+
* @param accessToken JWT access token. Optional if stored internally.
|
|
181
|
+
* @throws PpusshAuthError If the token is invalid or expired.
|
|
182
|
+
*/
|
|
183
|
+
async logoutAll(accessToken) {
|
|
184
|
+
const tokenToUse = accessToken ?? this._accessToken;
|
|
185
|
+
if (!tokenToUse) {
|
|
186
|
+
throw new Error("No accessToken provided and none stored. " +
|
|
187
|
+
"Call exchangeCode() first or pass accessToken explicitly.");
|
|
188
|
+
}
|
|
189
|
+
await this._http.request("POST", "/auth/logout", {
|
|
190
|
+
headers: { Authorization: `Bearer ${tokenToUse}` },
|
|
191
|
+
});
|
|
192
|
+
this._clearTokens();
|
|
193
|
+
}
|
|
194
|
+
// ── Session management ─────────────────────────────────────────────────────
|
|
195
|
+
/**
|
|
196
|
+
* Revoke a specific session by its ID.
|
|
197
|
+
*
|
|
198
|
+
* Uses DELETE /auth/sessions/{sessionId} — the user can only revoke their
|
|
199
|
+
* own sessions. Useful for "sign out of this device" UX in a session
|
|
200
|
+
* management screen.
|
|
201
|
+
*
|
|
202
|
+
* @param sessionId The UUID of the session to revoke (from getSessions()).
|
|
203
|
+
* @param accessToken JWT access token. Optional if stored internally.
|
|
204
|
+
* @throws PpusshAuthError If the token is invalid or the session does not
|
|
205
|
+
* belong to the authenticated user.
|
|
206
|
+
*/
|
|
207
|
+
async revokeSession(sessionId, accessToken) {
|
|
208
|
+
const tokenToUse = accessToken ?? this._accessToken;
|
|
209
|
+
if (!tokenToUse) {
|
|
210
|
+
throw new Error("No accessToken provided and none stored. " +
|
|
211
|
+
"Call exchangeCode() first or pass accessToken explicitly.");
|
|
212
|
+
}
|
|
213
|
+
await this._http.request("DELETE", `/auth/sessions/${sessionId}`, {
|
|
214
|
+
headers: { Authorization: `Bearer ${tokenToUse}` },
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
// ── User profile ───────────────────────────────────────────────────────────
|
|
218
|
+
/**
|
|
219
|
+
* Fetch the full user profile for an access token.
|
|
220
|
+
*
|
|
221
|
+
* If accessToken is omitted, the internally stored token from the last
|
|
222
|
+
* exchangeCode() or refresh() is used.
|
|
223
|
+
*
|
|
224
|
+
* @throws PpusshAuthError If the token is invalid or expired.
|
|
225
|
+
*/
|
|
226
|
+
async getUser(accessToken) {
|
|
227
|
+
const tokenToUse = accessToken ?? this._accessToken;
|
|
228
|
+
if (!tokenToUse) {
|
|
229
|
+
throw new Error("No accessToken provided and none stored. " +
|
|
230
|
+
"Call exchangeCode() first or pass accessToken explicitly.");
|
|
231
|
+
}
|
|
232
|
+
const response = await this._http.request("GET", "/users/me", {
|
|
233
|
+
headers: { Authorization: `Bearer ${tokenToUse}` },
|
|
234
|
+
});
|
|
235
|
+
return response.data;
|
|
236
|
+
}
|
|
237
|
+
// ── Entitlements & sessions ────────────────────────────────────────────────
|
|
238
|
+
/**
|
|
239
|
+
* List products the user has granted consent to (their entitlements).
|
|
240
|
+
*
|
|
241
|
+
* @param accessToken JWT access token. Optional if stored internally.
|
|
242
|
+
*/
|
|
243
|
+
async getEntitlements(accessToken) {
|
|
244
|
+
const tokenToUse = accessToken ?? this._accessToken;
|
|
245
|
+
if (!tokenToUse) {
|
|
246
|
+
throw new Error("No accessToken provided and none stored.");
|
|
247
|
+
}
|
|
248
|
+
const response = await this._http.request("GET", "/users/me/entitlements", {
|
|
249
|
+
headers: { Authorization: `Bearer ${tokenToUse}` },
|
|
250
|
+
});
|
|
251
|
+
return response.data;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* List all active sessions for the authenticated user.
|
|
255
|
+
*
|
|
256
|
+
* @param accessToken JWT access token. Optional if stored internally.
|
|
257
|
+
*/
|
|
258
|
+
async getSessions(accessToken) {
|
|
259
|
+
const tokenToUse = accessToken ?? this._accessToken;
|
|
260
|
+
if (!tokenToUse) {
|
|
261
|
+
throw new Error("No accessToken provided and none stored.");
|
|
262
|
+
}
|
|
263
|
+
const response = await this._http.request("GET", "/users/me/sessions", {
|
|
264
|
+
headers: { Authorization: `Bearer ${tokenToUse}` },
|
|
265
|
+
});
|
|
266
|
+
return response.data;
|
|
267
|
+
}
|
|
268
|
+
// ── Internal token management ──────────────────────────────────────────────
|
|
269
|
+
_storeTokens(token) {
|
|
270
|
+
this._accessToken = (0, types_1.effectiveAccessToken)(token);
|
|
271
|
+
this._refreshToken = token.refresh_token;
|
|
272
|
+
this._tokenExpiresAt = new Date(Date.now() + token.expires_in * 1000);
|
|
273
|
+
}
|
|
274
|
+
_clearTokens() {
|
|
275
|
+
this._accessToken = null;
|
|
276
|
+
this._refreshToken = null;
|
|
277
|
+
this._tokenExpiresAt = null;
|
|
278
|
+
}
|
|
279
|
+
/** The currently stored access token, if any. */
|
|
280
|
+
get accessToken() {
|
|
281
|
+
return this._accessToken;
|
|
282
|
+
}
|
|
283
|
+
/** The currently stored refresh token, if any. */
|
|
284
|
+
get refreshToken() {
|
|
285
|
+
return this._refreshToken;
|
|
286
|
+
}
|
|
287
|
+
/** UTC Date at which the stored access token expires, if known. */
|
|
288
|
+
get tokenExpiresAt() {
|
|
289
|
+
return this._tokenExpiresAt;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
exports.AccountsNamespace = AccountsNamespace;
|
|
293
|
+
//# sourceMappingURL=namespace.js.map
|