@productcraft/platform-auth 0.1.3 → 1.0.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 CHANGED
@@ -1,283 +1,189 @@
1
1
  # @productcraft/platform-auth
2
2
 
3
- Typed Node.js SDK for [ProductCraft Platform-Auth](https://productcraft.co) — PlatformUser sign-in / sign-up / password reset, workspaces, members + invites, workspace roles + IAM policies, workspace API keys, service activation, and the `/v1/introspect` endpoint every other ProductCraft backend uses to resolve permissions.
3
+ Node.js / TypeScript client for the **ProductCraft Platform API**: workspaces, members and invites, roles and IAM policies, workspace API keys, service activation, audit logs, and the introspection endpoints every ProductCraft backend uses to resolve permissions.
4
4
 
5
- ```bash
5
+ Every method, type, error class and mock is generated from the production OpenAPI spec. Upgrading from 0.x? See the [1.0 migration guide](https://github.com/clauderanelagh/productcraft-node/blob/main/docs/migrations/platform-auth-1.0.0.md).
6
+
7
+ ## Install
8
+
9
+ ```sh
6
10
  npm install @productcraft/platform-auth
7
11
  ```
8
12
 
9
- Server-side only. The SDK ships a Platform API Key in the `Authorization` header — never embed it in a browser bundle.
13
+ Node 20.3 or later, Bun, Deno and edge runtimes. Server-side: it sends a workspace key or a platform session. Or install the umbrella package for every product: `npm install productcraft`.
10
14
 
11
15
  ## Which SDK do I want?
12
16
 
13
17
  | If your goal is to … | Reach for |
14
18
  |---|---|
15
- | Authenticate **your own end-users** in a product you're building (Auth apps) | [`@productcraft/auth`](https://www.npmjs.com/package/@productcraft/auth) |
16
- | Authenticate **your team** into the ProductCraft console + manage workspaces | **this package** |
19
+ | Sign in **your own end-users** in a product you build on ProductCraft | [`@productcraft/auth`](https://www.npmjs.com/package/@productcraft/auth) |
20
+ | Manage **your ProductCraft account and workspaces**: keys, members, roles, policies, services | **this package** |
17
21
 
18
- The two are easy to confuse because both have "auth" in the name. Platform-Auth owns *the ProductCraft account you use to log into productcraft.co*. Auth owns *the end-users of products you build on the platform*. Distinct databases, distinct signing keys.
22
+ Both have "auth" in the name. Platform owns the ProductCraft account you use to sign in to the console; Auth owns the end-users of the products you build. They are separate databases with separate signing keys.
19
23
 
20
24
  ## Quick start
21
25
 
22
26
  ```ts
23
27
  import { PlatformAuth } from "@productcraft/platform-auth";
24
28
 
25
- // Reads PRODUCTCRAFT_API_KEY from the environment
26
- const platformAuth = new PlatformAuth();
29
+ // Reads PRODUCTCRAFT_API_KEY from the environment.
30
+ const platform = new PlatformAuth();
27
31
 
28
- // Resolve the caller's effective policy + enabled services for a workspace
29
- const { data } = await platformAuth.client.GET(
30
- "/v1/introspect/workspaces/{workspace_id}",
31
- { params: { path: { workspace_id: "<workspace-uuid>" } } },
32
- );
32
+ // Which workspace and permissions does this key carry?
33
+ const me = await platform.apiKeys.self();
34
+ console.log(me.workspace_id, me.name, me.allowed);
33
35
 
34
- // data.policy — flat array of IAM-style { effect, actions, resources } statements
35
- // data.services — string[] of enabled services (mail / waitlist / social / auth / …)
36
+ // Workspace routes take the workspace slug as the first argument.
37
+ const ws = await platform.workspaces.retrieve("acme");
38
+
39
+ // Pages are fetched as you iterate.
40
+ for await (const key of platform.workspaces.apiKeys.list(ws.slug, { include_revoked: "false" })) {
41
+ console.log(key.id, key.name, key.status);
42
+ }
36
43
  ```
37
44
 
38
- The `client` is an [`openapi-fetch`](https://openapi-ts.dev/openapi-fetch/) instance bound to `https://api.auth.productcraft.co` and your auth credential.
45
+ Platform routes name the workspace by **slug**, positionally (`platform.workspaces.members.list("acme")`). The one exception is `platform.stats.retrieve()`, which uses the workspace id: pass `workspaceId`, or let a workspace key resolve it once per process.
39
46
 
40
- ### Auth modes
47
+ ## Credentials
41
48
 
42
- | Mode | When to use | Mint/rotate/delete surfaces |
49
+ | Credential | Pass | Use it for |
43
50
  |---|---|---|
44
- | `apiKey` (`pcft_live_*`) | Backend-to-backend reads + introspect calls | ❌ — mint requires a human session |
45
- | `bearer` (PlatformUser JWT) | Anything administrative: workspace create/delete, role/policy authoring, PAK minting, invite mutations | ✅ |
46
- | `cookie` (`auth_token`) | Server-side route handlers running behind the console session | ✅ |
47
-
48
- PAKs are workspace-scoped credentials — they read + introspect freely against the workspace they're bound to but cannot mint another PAK or reshape the human-admin surface. Cross-workspace probes by a PAK return the uniform non-member shape (no slug → UUID oracle).
51
+ | Workspace API key (`pcft_live_…`) | `apiKey`, or `PRODUCTCRAFT_API_KEY` in the environment | Backend reads and introspection for the workspace the key belongs to |
52
+ | Platform session (a signed-in person) | `auth: { type: "bearer", token }` | Administration: creating workspaces, minting and revoking keys, roles, policies, invites |
49
53
 
50
- ## Credentials from the environment
54
+ ```ts
55
+ import { PlatformAuth } from "@productcraft/platform-auth";
51
56
 
52
- Server-side, omit `auth` and the client reads your workspace key from the environment:
57
+ const anon = new PlatformAuth();
58
+ const { access_token } = await anon.auth.signin({ identifier: "ada@example.com", password: process.env.PASSWORD!, session_duration: 3600 });
59
+ const admin = new PlatformAuth({ auth: { type: "bearer", token: access_token } });
53
60
 
54
- ```bash
55
- # .env
56
- PRODUCTCRAFT_API_KEY=pcft_live_...
61
+ const minted = await admin.workspaces.apiKeys.create("acme", { name: "Production mail", preset: "mail-send", expires_in_days: 90 });
62
+ console.log(minted.token); // shown once: store it in your secret manager
63
+ await admin.auth.logoutCurrent();
57
64
  ```
58
65
 
59
- - An explicit `auth` always wins over the environment.
60
- - Hosts are never read from the environment. Override them in code with `baseUrl`.
61
- - This is a server-only fallback. Never put a key in `NEXT_PUBLIC_*` or any other variable a bundler inlines into browser code.
66
+ A workspace key cannot mint another key or reshape roles and policies; those calls need a person's session. An explicit `auth` always wins over the environment. The environment fallback is server-only: never put a key in `NEXT_PUBLIC_*` or any variable a bundler inlines into browser code. A keyed client constructed in a browser throws unless you set `dangerouslyAllowBrowser: true`.
62
67
 
63
- ## Configuration
68
+ ## Introspection (for backends that accept workspace keys)
64
69
 
65
70
  ```ts
66
- new PlatformAuth({
67
- // Omit to use PRODUCTCRAFT_API_KEY from the environment
68
- auth: { type: "apiKey", key: "pcft_live_..." }
69
- | { type: "bearer", token: "eyJ..." }
70
- | { type: "cookie", value: "auth_token=..." },
71
- baseUrl: "https://api.auth.example.test", // optional override
72
- fetch: customFetch, // optional
73
- });
74
- ```
71
+ import { PlatformAuth } from "@productcraft/platform-auth";
75
72
 
76
- ## Common operations
73
+ // Resolve the key a request carried to its workspace and policy.
74
+ declare const incomingKey: string;
75
+ const caller = new PlatformAuth({ apiKey: incomingKey });
76
+ const { is_valid, workspace_id, policy } = await caller.introspect.apiKey();
77
+ ```
77
78
 
78
- ### Authentication (PlatformUser sign-in)
79
+ <!-- generated:start (scripts/gen-sdk.mjs; edits between the markers are overwritten) -->
79
80
 
80
- ```ts
81
- // Sign in
82
- await platformAuth.client.POST(
83
- "/v1/auth/signin",
84
- { body: { identifier: "you@example.com", password: "..." } },
85
- );
86
-
87
- // Refresh tokens
88
- await platformAuth.client.POST("/v1/auth/refresh", { body: { refresh_token } });
89
-
90
- // Logout (single session) — or /v1/auth/logout to revoke every refresh token
91
- await platformAuth.client.POST("/v1/auth/logout-current", { body: { refresh_token } });
92
-
93
- // Password reset
94
- await platformAuth.client.POST(
95
- "/v1/auth/password/request-reset",
96
- { body: { email: "you@example.com" } },
97
- );
98
-
99
- await platformAuth.client.POST(
100
- "/v1/auth/password/reset",
101
- { body: { token, new_password: "..." } },
102
- );
103
- ```
81
+ ## Resources
104
82
 
105
- ### Introspect — used by every other ProductCraft backend
83
+ Full reference: [api.md](./api.md).
106
84
 
107
- ```ts
108
- // Resolve the bearer of a token: returns account + email_verified_at + workspaces.
109
- // Accepts a `pcft_live_*` PAK too — the response carries a synthetic verified
110
- // account block so downstream `RequireEmailVerifiedGuard`-style gates pass.
111
- await platformAuth.client.GET("/v1/introspect");
112
-
113
- // Resolve effective policy + enabled services for one workspace
114
- await platformAuth.client.GET(
115
- "/v1/introspect/workspaces/{workspace_id}",
116
- { params: { path: { workspace_id: "<workspace-uuid>" } } },
117
- );
118
-
119
- // Resolve a PAK — POST, not GET. Used internally by services that accept
120
- // `pcft_live_*` on routes. Caller's bearer is the PAK being introspected;
121
- // the body is unused (sent as `{}`).
122
- await platformAuth.client.POST("/v1/introspect/api-key");
123
- ```
85
+ - `platform.account`: `erase`
86
+ - `platform.apiKeys`: `self`
87
+ - `platform.auth`: `logout`, `logoutCurrent`, `password.requestReset`, `password.reset`, `refresh`, `signin`, `signup`
88
+ - `platform.introspect`: `apiKey`, `retrieve`, `workspaces.retrieve`
89
+ - `platform.me`: `avatar.clear`, `avatar.upload`, `hiddenGuides.hide`, `hiddenGuides.list`, `hiddenGuides.reset`, `hiddenGuides.unhide`, `profile.update`, `retrieve`
90
+ - `platform.oauth`: `connections.delete`, `connections.list`, `workspaces.connections.delete`, `workspaces.connections.list`, `workspaces.connections.restore`
91
+ - `platform.sessions`: `delete`, `list`, `revokeOthers`
92
+ - `platform.stats`: `retrieve`
93
+ - `platform.verification`: `resend`, `verify`
94
+ - `platform.wellKnown`: `jwks`, `oauthAuthorizationServer`
95
+ - `platform.workspaces`: `apiKeys.create`, `apiKeys.delete`, `apiKeys.list`, `apiKeys.presets.list`, `apiKeys.preview`, `apiKeys.retrieve`, `apiKeys.rotate`, `apiKeys.update`, `apiKeys.updateBindings`, `auditFeed.list`, `auditLogs.export`, `auditLogs.list`, `avatar.clear`, `avatar.upload`, `create`, `delete`, `invites.accept`, `invites.create`, `invites.delete`, `invites.list`, `list`, `members.delete`, `members.list`, `members.updateRole`, `policies.actions.catalog`, `policies.create`, `policies.delete`, `policies.list`, `policies.retrieve`, `policies.update`, `retrieve`, `roles.create`, `roles.delete`, `roles.list`, `roles.retrieve`, `roles.update`, `services.disable`, `services.enable`, `services.list`, `services.updateSettings`, `update`
124
96
 
125
- The latter two are cached by every consuming service for ~60 seconds. Reach for them from a backend gate; never from a hot request path.
97
+ ## Pagination
126
98
 
127
- ### Workspaces
99
+ List methods return a `PagePromise`: `for await` walks every item across pages, `await` gives one page.
128
100
 
129
101
  ```ts
130
- // List — for a PAK caller, this returns exactly the workspace the PAK is
131
- // bound to (member_role / member_role_id are null on that lane).
132
- await platformAuth.client.GET("/v1/workspaces");
102
+ import { PlatformAuth } from "@productcraft/platform-auth";
103
+ const platform = new PlatformAuth({ apiKey: process.env.PRODUCTCRAFT_API_KEY });
104
+ for await (const item of platform.oauth.connections.list({ limit: 50 })) console.log(item);
105
+ const page = await platform.oauth.connections.list();
106
+ if (page.hasNextPage()) await page.getNextPage();
107
+ const all = await platform.oauth.connections.list().toArray({ maxItems: 500 });
108
+ ```
133
109
 
134
- // Create — cookie/JWT only. PAKs cannot create workspaces.
135
- await platformAuth.client.POST(
136
- "/v1/workspaces",
137
- { body: { display_name: "Acme", slug: "acme" } },
138
- );
110
+ ## Errors
139
111
 
140
- // Get / update by slug
141
- await platformAuth.client.GET("/v1/workspaces/{workspace_slug}", { ... });
112
+ Every failure is a class you can catch. `APIError` carries `status`, `code`, `message`, `errors[]`, `requestId` and `docUrl`.
142
113
 
143
- // Delete — cookie/JWT only. PAKs cannot delete workspaces.
144
- await platformAuth.client.DELETE("/v1/workspaces/{workspace_slug}", { ... });
114
+ ```ts
115
+ import { PlatformAuth, NotFoundError, RateLimitError, APIError } from "@productcraft/platform-auth";
116
+ const platform = new PlatformAuth();
117
+ try {
118
+ await platform.introspect.workspaces.retrieve("00000000-0000-0000-0000-000000000000");
119
+ } catch (err) {
120
+ if (err instanceof NotFoundError) console.log(err.code, err.requestId, err.docUrl);
121
+ else if (err instanceof RateLimitError) console.log(err.retryAfter);
122
+ else if (err instanceof APIError) console.log(err.status, err.errors);
123
+ else throw err;
124
+ }
145
125
  ```
146
126
 
147
- ### Members, invites, roles
127
+ ## Retries and timeouts
128
+
129
+ Network errors, 408, 429, 5xx and `409 IDEMPOTENCY_IN_PROGRESS` are retried twice by default with exponential backoff and jitter, honouring `Retry-After` up to 60 s. A POST or PATCH without an idempotency key is never retried. Each attempt times out after 60 s, so the worst case with defaults is about 3 minutes; pass `signal: AbortSignal.timeout(ms)` for an overall deadline.
148
130
 
149
131
  ```ts
150
- // Members
151
- await platformAuth.client.GET("/v1/workspaces/{workspace_slug}/members", { ... });
152
- await platformAuth.client.PATCH(
153
- "/v1/workspaces/{workspace_slug}/members/{account_id}/role",
154
- { params: { ... }, body: { role_id: "role_..." } },
155
- );
156
-
157
- // Invites
158
- await platformAuth.client.POST(
159
- "/v1/workspaces/{workspace_slug}/invites",
160
- { params: { ... }, body: { email: "...", role_id: "..." } },
161
- );
162
- await platformAuth.client.POST("/v1/workspaces/invites/accept", { body: { code } });
163
-
164
- // Roles
165
- await platformAuth.client.POST(
166
- "/v1/workspaces/{workspace_slug}/roles",
167
- { params: { ... }, body: { name: "billing-admin", policy_ids: ["pol_..."] } },
168
- );
132
+ import { PlatformAuth } from "@productcraft/platform-auth";
133
+ const platform = new PlatformAuth({ maxRetries: 3, timeout: 20_000 });
134
+ await platform.oauth.connections.list({}, { maxRetries: 0, signal: AbortSignal.timeout(5_000) });
169
135
  ```
170
136
 
171
- ### IAM policies
137
+ ## Idempotency
138
+
139
+ Writes that support it send an `Idempotency-Key` automatically and reuse it on every retry. Pass your own to make a retry across processes safe. If the API's idempotency store is unavailable it runs the request without it (fail-open), so a retry during that window can execute twice.
172
140
 
173
141
  ```ts
174
- // List the canonical permission catalogue (what verbs exist for each service)
175
- await platformAuth.client.GET(
176
- "/v1/workspaces/{workspace_slug}/policies/actions/catalog",
177
- { ... },
178
- );
179
-
180
- // Create a managed policy. Cookie/JWT only.
181
- await platformAuth.client.POST(
182
- "/v1/workspaces/{workspace_slug}/policies",
183
- {
184
- params: { ... },
185
- body: {
186
- name: "mail-read-only",
187
- description: "Read-only access to all Mail resources",
188
- policy: [
189
- { effect: "allow", actions: ["mail.read", "mail.list"], resources: ["*"] },
190
- ],
191
- },
192
- },
193
- );
142
+ import { PlatformAuth } from "@productcraft/platform-auth";
143
+ const platform = new PlatformAuth({ apiKey: process.env.PRODUCTCRAFT_API_KEY });
144
+ const workspace = await platform.workspaces.create({ display_name: "Acme Corp", slug: "acme-corp" }, { idempotencyKey: "order-1234" });
194
145
  ```
195
146
 
196
- ### Workspace API keys (PATs)
197
-
198
- PATs bind to one or more managed `workspace_policy` rows. Author the policy first, then mint the key with its id. The inline `policy: [...]` shape is **not** accepted — `policy_ids: [<uuid>]` is required.
147
+ ## Raw responses
199
148
 
200
- Mint / rotate / revoke require a cookie or JWT — a PAK cannot mint another PAK.
149
+ Every returned object has a non-enumerable `lastResponse` (`requestId`, `status`, `headers`). `.withResponse()` adds the `Response` and parsed rate-limit headers.
201
150
 
202
151
  ```ts
203
- // 1. Author (or reuse) a managed policy
204
- const { data: policy } = await platformAuth.client.POST(
205
- "/v1/workspaces/{workspace_slug}/policies",
206
- {
207
- params: { path: { workspace_slug: "acme" } },
208
- body: {
209
- name: "ci-staging-deploy",
210
- description: "Auth + Mail for the staging pipeline",
211
- policy: [
212
- // Permission action prefixes rebranded (heimdall.* → auth.*, envoi.* → mail.*),
213
- // but resource-URN namespaces intentionally stay pcft:heimdall: / pcft:envoi: —
214
- // the services still emit the old resource prefixes.
215
- { effect: "allow", actions: ["auth.create", "auth.read", "auth.update"], resources: ["pcft:heimdall:app/*"] },
216
- { effect: "allow", actions: ["mail.read", "mail.send"], resources: ["*"] },
217
- ],
218
- },
219
- },
220
- );
221
-
222
- // 2. Mint a PAT that binds to that policy
223
- const { data: pak } = await platformAuth.client.POST(
224
- "/v1/workspaces/{workspace_slug}/api-keys",
225
- {
226
- params: { path: { workspace_slug: "acme" } },
227
- body: {
228
- name: "github-actions-staging-deploy",
229
- description: "Deploys acme-staging app from CI",
230
- policy_ids: [policy!.id],
231
- },
232
- },
233
- );
234
- // pak.token — the plaintext `pcft_live_*` value, returned exactly once
235
- // pak.record — public metadata (id, prefix, policies, last_used_at, …)
236
-
237
- // Replace the bound policies on an existing PAK (re-runs caller-narrowing
238
- // against the merged statement set)
239
- await platformAuth.client.PATCH(
240
- "/v1/workspaces/{workspace_slug}/api-keys/{id}/bindings",
241
- { params: { ... }, body: { policy_ids: ["<new-policy-uuid>"] } },
242
- );
152
+ import { PlatformAuth } from "@productcraft/platform-auth";
153
+ const platform = new PlatformAuth({ apiKey: process.env.PRODUCTCRAFT_API_KEY });
154
+ const id = "9b2f6c1e-0d6a-4a63-9a43-2f1c3e4d5a6b";
155
+ const obj = await platform.introspect.workspaces.retrieve(id);
156
+ console.log(obj.lastResponse.requestId);
157
+ const { data, response, requestId, rateLimit } = await platform.introspect.workspaces.retrieve(id).withResponse();
158
+ console.log(data, response.status, requestId, rateLimit?.remaining);
243
159
  ```
244
160
 
245
- ### Service activation
161
+ ## Testing
162
+
163
+ `@productcraft/platform-auth/testing` exports `createMockFetch()`, a `fetch` that answers every operation with its spec example. Override one operation by name.
246
164
 
247
165
  ```ts
248
- // Enable / disable a service for the workspace (mail, waitlist, social, auth, …)
249
- await platformAuth.client.PUT(
250
- "/v1/workspaces/{workspace_slug}/services/{service}",
251
- { params: { ... }, body: { enabled: true } },
252
- );
253
-
254
- // Per-service workspace settings
255
- await platformAuth.client.PUT(
256
- "/v1/workspaces/{workspace_slug}/services/{service}/settings",
257
- { ... },
258
- );
166
+ import { PlatformAuth } from "@productcraft/platform-auth";
167
+ import { createMockFetch } from "@productcraft/platform-auth/testing";
168
+ const fetch = createMockFetch();
169
+ const platform = new PlatformAuth({ apiKey: "test", workspaceId: "9b2f6c1e-0d6a-4a63-9a43-2f1c3e4d5a6b", fetch });
170
+ await platform.oauth.connections.list();
171
+ console.log(fetch.calls.length);
259
172
  ```
260
173
 
261
- ### Audit
174
+ ## Escape hatch
175
+
176
+ `.client` is the typed `openapi-fetch` client over the same credential and base URL (no retries). Use it for anything the resource methods do not cover.
262
177
 
263
178
  ```ts
264
- // Read the workspace audit log
265
- await platformAuth.client.GET(
266
- "/v1/workspaces/{workspace_slug}/audit-logs",
267
- { params: { ... }, query: { limit: 100 } },
268
- );
269
-
270
- // Streaming-friendly "recent activity" feed (for the console UI)
271
- await platformAuth.client.GET(
272
- "/v1/workspaces/{workspace_slug}/audit-feed",
273
- { ... },
274
- );
179
+ import { PlatformAuth } from "@productcraft/platform-auth";
180
+ const platform = new PlatformAuth({ apiKey: process.env.PRODUCTCRAFT_API_KEY });
181
+ const { data, error } = await platform.client.GET("/.well-known/jwks.json");
182
+ console.log(data, error);
275
183
  ```
276
184
 
277
- ## How this SDK is built
278
-
279
- Generated from the live OpenAPI spec at `https://api.auth.productcraft.co/docs-json` via [`openapi-typescript`](https://openapi-ts.dev/) + [`openapi-fetch`](https://openapi-ts.dev/openapi-fetch/). The nightly `spec-refresh` workflow opens a PR whenever the spec changes.
185
+ <!-- generated:end -->
280
186
 
281
187
  ## License
282
188
 
283
- [MIT](https://github.com/clauderanelagh/productcraft-node/blob/main/LICENSE).
189
+ MIT