@faable/auth-sdk 2.5.95 → 2.6.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.
@@ -24,6 +24,10 @@ jobs:
24
24
  node-version: lts/*
25
25
  - run: npm ci
26
26
  - run: npm run build
27
+ # Fails the build if the version sentinels are not in dist/ verbatim
28
+ # (the release replacement would miss them and the SDK would ship
29
+ # anonymous) or if anything in dist/ imports `node:*` (browser-unsafe).
30
+ - run: npm run test:pack
27
31
  - run: npm run release
28
32
  env:
29
33
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
package/.releaserc CHANGED
@@ -18,6 +18,27 @@
18
18
  "repositoryUrl": "https://x-access-token:${GITHUB_TOKEN}@github.com/faablecloud/auth-sdk.git",
19
19
  "plugins": [
20
20
  "@semantic-release/commit-analyzer",
21
+ [
22
+ "semantic-release-replace-plugin",
23
+ {
24
+ "replacements": [
25
+ {
26
+ "files": [
27
+ "dist/*.js"
28
+ ],
29
+ "from": "0.0.0-dev",
30
+ "to": "${nextRelease.version}"
31
+ },
32
+ {
33
+ "files": [
34
+ "dist/*.js"
35
+ ],
36
+ "from": "0000000dev",
37
+ "to": "${nextRelease.gitHead.slice(0, 7)}"
38
+ }
39
+ ]
40
+ }
41
+ ],
21
42
  "@semantic-release/release-notes-generator",
22
43
  [
23
44
  "@semantic-release/npm",
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
  <a href="https://faable.com">
3
3
  <h1 align="center">Faable Auth SDK</h1>
4
4
  </a>
5
- <p align="center">Server-side client for the FaableAuth administrative REST API.</p>
5
+ <p align="center">Client for the FaableAuth REST API — management from a server, session and login flows from a browser or CLI.</p>
6
6
  </p>
7
7
 
8
8
  <p align="center">
@@ -11,9 +11,22 @@
11
11
  </a>
12
12
  </p>
13
13
 
14
- Programmatically manage FaableAuth users, teams, connections and clients through the FaableAuth REST API.
14
+ Programmatically manage FaableAuth users, teams, connections and clients through the FaableAuth REST API — and, with the right strategy, call the tenant's session and login endpoints from a browser or a CLI.
15
15
 
16
- > ⚠️ **Server-side only.** This SDK uses administrative credentials. Never ship it to a browser or any untrusted runtime.
16
+ > ⚠️ **Where it can run depends on the credential, not on the package.** The
17
+ > SDK itself has no Node-only dependencies and bundles for the browser. What
18
+ > you must never ship to a browser or any untrusted runtime is a
19
+ > **`client_secret`** or an **API key** — those grant management scopes over
20
+ > the whole tenant. A user's own bearer token or the browser's session cookie
21
+ > only grant what that user can do, and those are fine client-side.
22
+
23
+ | Strategy | Credential | Grants | Where |
24
+ | ----------------------- | --------------------------------- | ----------------------------------------------------------------- | --------------------------------------- |
25
+ | `authClientCredentials` | `client_id` + **`client_secret`** | The management scopes of the client (full catalog unless narrowed) | **Server only** |
26
+ | `authApikey` | **API key** (`fak_…`) | The management scopes of the key | **Server only** |
27
+ | `authBearer` | A token the caller already holds | Whatever that token's scopes and subject allow | Anywhere the token itself may live |
28
+ | `authCookie` | The browser's session cookie | The signed-in user's own session (`/me`, MFA, passkeys…) | Browser, same-origin with the auth host |
29
+ | _(none)_ | — | Public endpoints only (login, passwordless start, device flow…) | Anywhere |
17
30
 
18
31
  ---
19
32
 
@@ -76,15 +89,89 @@ const api = FaableAuthApi.create({
76
89
  });
77
90
  ```
78
91
 
92
+ ### Bearer token
93
+
94
+ For a token you already hold — a user's access token in a CLI, a token minted
95
+ elsewhere. The SDK does not mint or refresh it; whoever owns the token does.
96
+ `token` may be a getter (sync or async): it is consulted on **every** request,
97
+ so a getter that returns the current token keeps a long-lived client correct
98
+ when the token rotates.
99
+
100
+ ```ts
101
+ import { FaableAuthApi, authBearer } from "@faable/auth-sdk";
102
+
103
+ const api = FaableAuthApi.create({
104
+ domain: "https://<your-account>.auth.faable.link",
105
+ authStrategy: authBearer,
106
+ auth: { token: () => session.accessToken },
107
+ });
108
+ ```
109
+
110
+ What the calls may do is whatever the token's subject and scopes allow — a
111
+ user token reaches the user's own resources, a management token reaches the
112
+ management API.
113
+
114
+ ### Session cookie (browser)
115
+
116
+ For pages served under the tenant's auth host that authenticate with the
117
+ browser's session cookie (the hosted login pages: `/me`, MFA, passkeys). No
118
+ config: the browser attaches the cookie itself; the strategy makes the request
119
+ `withCredentials` so it also does cross-origin, where the auth server must
120
+ allow the page's origin for credentials.
121
+
122
+ ```ts
123
+ import { FaableAuthApi, authCookie } from "@faable/auth-sdk";
124
+
125
+ const api = FaableAuthApi.create({
126
+ domain: "https://<your-account>.auth.faable.link",
127
+ authStrategy: authCookie,
128
+ });
129
+ ```
130
+
131
+ ### No credentials
132
+
133
+ Omit `auth` and `authStrategy` and the client is anonymous — right for the
134
+ public endpoints (login, `/passwordless/start`, the device flow, OIDC
135
+ discovery). Those are not on the generated client; call them through the
136
+ fetcher:
137
+
138
+ ```ts
139
+ const api = FaableAuthApi.create({ domain: "https://<your-account>.auth.faable.link" });
140
+ await api.fetcher.post("/passwordless/start", { email, client_id });
141
+ ```
142
+
143
+ ### Identifying your integration
144
+
145
+ Every request carries `x-faable-client: auth-sdk/<version>+<sha>` so the
146
+ auth server can attribute traffic. An app that wraps the SDK can report as
147
+ itself instead — the value you pass replaces the default, on data calls and on
148
+ the token request alike:
149
+
150
+ ```ts
151
+ FaableAuthApi.create({ domain, clientInfo: { client: "my-app/1.4.0", name: "my-app" } });
152
+ ```
153
+
154
+ ### Per-call options
155
+
156
+ Every fetcher method takes `timeout`, `responseType` and `retry` for that one
157
+ call. Retries only ever apply to GET (a replayed write could duplicate a
158
+ resource), and `timeout` and `retry` multiply — a short deadline on a hot path
159
+ wants `retry: false` next to it:
160
+
161
+ ```ts
162
+ await api.fetcher.get(`/user/${id}`, { timeout: 800, retry: false });
163
+ ```
164
+
79
165
  ### Constructor options
80
166
 
81
167
  | Option | Type | Description |
82
168
  | -------------- | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
83
169
  | `domain` | string | **Required.** The account host to target, e.g. `https://<your-account>.auth.faable.link`. No default. The protocol is optional — `<your-account>.auth.faable.link` and `https://<your-account>.auth.faable.link` are equivalent. |
84
- | `authStrategy` | strategy builder | Auth strategy (`authClientCredentials` \| `authApikey`), re-exported from `@faable/auth-sdk`. Defaults to `authClientCredentials`. |
170
+ | `authStrategy` | strategy builder | Auth strategy (`authClientCredentials` \| `authApikey` \| `authBearer` \| `authCookie`), re-exported from `@faable/auth-sdk`. Defaults to `authClientCredentials` when `auth` is given; anonymous when neither is. |
85
171
  | `auth` | config | Config for the chosen strategy. Its shape is inferred from `authStrategy` (e.g. `{ client_id, client_secret }`). |
86
172
  | `debug` | boolean | Enables verbose logging in the underlying fetcher. |
87
173
  | `headers` | `{ team_id?; account_id? }` | Advanced / testing escape hatch — target `domain` but have the server resolve a different team/account (sent as `x-faable-team` / `x-faableauth-account`). |
174
+ | `clientInfo` | `{ client?; name?; instance? }` | How this integration identifies itself (`x-faable-client` / `x-faable-instance`). `client` replaces the SDK's own `auth-sdk/<version>+<sha>`. |
88
175
 
89
176
  ---
90
177
 
@@ -2,7 +2,7 @@ import { getDomain, isPlainObject, requireId } from "./helpers.js";
2
2
  import { FaableApiError } from "./error_handler.js";
3
3
  import { GeneratedFaableAuthApi } from "./api/generated-client.js";
4
4
  import { authClientCredentials } from "@faable/sdk-base";
5
- import { version } from "./version.js";
5
+ import { SDK_CLIENT } from "./version.js";
6
6
  // auth-sdk exists to drive the Faable management API, so a client_credentials
7
7
  // client should "just work" against it from `domain` alone. The management API
8
8
  // audience is `faable:management:<account_id>`, where `<account_id>` is this
@@ -67,23 +67,27 @@ export class FaableAuthApi extends GeneratedFaableAuthApi {
67
67
  ...params,
68
68
  auth,
69
69
  // Identify ourselves as a first-party client (`x-faable-client:
70
- // auth-sdk/<version>`, surfaced as the `client`/`client_version` tags),
71
- // and forward any consumer-supplied `name`/`instance` for per-replica
72
- // attribution. Routing this through `clientInfo` makes it ride on the
73
- // `/oauth/token` call too (sdk-base stamps both fetchers from the same
74
- // identity). A caller-set `clientInfo.client` still wins.
70
+ // auth-sdk/<version>+<sha>`, surfaced as the `client`/`client_version`/
71
+ // `client_sha` tags), and forward any consumer-supplied `name`/`instance`
72
+ // for per-replica attribution. Routing this through `clientInfo` makes
73
+ // it ride on the `/oauth/token` call too (sdk-base stamps both fetchers
74
+ // from the same identity). A caller-set `clientInfo.client` wins — the
75
+ // hosted login pages and the CLI wrap this SDK and must report as
76
+ // themselves, not as auth-sdk.
77
+ //
78
+ // Nothing is set as a static fetcher header any more: sdk-base gives
79
+ // explicit `fetcher.headers` precedence over `clientInfo`, so the old
80
+ // backward-compat copy of this value silently overrode every consumer's
81
+ // `clientInfo.client` (and left `/oauth/token` reporting a different
82
+ // client than the data calls). The sdk-base this package requires has
83
+ // honoured `clientInfo` since 1.5.x, so the fallback had no job left.
75
84
  clientInfo: {
76
- client: `auth-sdk/${version}`,
85
+ client: SDK_CLIENT,
77
86
  ...params?.clientInfo,
78
87
  },
79
88
  fetcher: {
80
89
  ...params?.fetcher,
81
90
  headers: {
82
- // Also set `x-faable-client` as a static fetcher header for
83
- // backward-compat: if this runs against an older @faable/sdk-base
84
- // that ignores `clientInfo`, data requests still carry it (only the
85
- // new `/oauth/token` attribution needs the newer sdk-base).
86
- "x-faable-client": `auth-sdk/${version}`,
87
91
  ...params?.fetcher?.headers,
88
92
  ...(params?.headers?.team_id && {
89
93
  "x-faable-team": params.headers.team_id,
@@ -377,6 +377,39 @@ export declare abstract class GeneratedFaableAuthApi extends FaableApi {
377
377
  next_in_queue_kid: string;
378
378
  kids_active: string[];
379
379
  }>;
380
+ /**
381
+ * `GET /sdk-health/{account_id}` — operationId: `account/sdk-health`
382
+ *
383
+ * Library health for this account
384
+ */
385
+ accountSdkHealth(account_id: string): Promise<{
386
+ libraries: {
387
+ name: string;
388
+ package: string;
389
+ latest: string | null;
390
+ installs: {
391
+ version: string;
392
+ sha: string | null;
393
+ app: {
394
+ client_id: string;
395
+ name: string;
396
+ } | null;
397
+ lag: "unknown" | "patch" | "current" | "minor" | "major";
398
+ legacy_unversioned: boolean;
399
+ first_seen: string;
400
+ last_seen: string;
401
+ }[];
402
+ }[];
403
+ traffic: {
404
+ days: number;
405
+ total: number;
406
+ by_class: {
407
+ [key: string]: number;
408
+ };
409
+ } | null;
410
+ latest_checked_at: string | null;
411
+ retention_days: number;
412
+ }>;
380
413
  /**
381
414
  * `POST /account/{account_id}` — operationId: `account/update`
382
415
  *
@@ -67,6 +67,15 @@ export class GeneratedFaableAuthApi extends FaableApi {
67
67
  accountRotateKeys(data) {
68
68
  return this.fetcher.post(`/account/keys/rotate`, data);
69
69
  }
70
+ /**
71
+ * `GET /sdk-health/{account_id}` — operationId: `account/sdk-health`
72
+ *
73
+ * Library health for this account
74
+ */
75
+ accountSdkHealth(account_id) {
76
+ requireId("account_id", account_id);
77
+ return this.fetcher.get(`/sdk-health/${account_id}`);
78
+ }
70
79
  /**
71
80
  * `POST /account/{account_id}` — operationId: `account/update`
72
81
  *
@@ -1469,6 +1469,26 @@ export interface paths {
1469
1469
  patch?: never;
1470
1470
  trace?: never;
1471
1471
  };
1472
+ "/sdk-health/{account_id}": {
1473
+ parameters: {
1474
+ query?: never;
1475
+ header?: never;
1476
+ path?: never;
1477
+ cookie?: never;
1478
+ };
1479
+ /**
1480
+ * Library health for this account
1481
+ * @description Which Faable client libraries this account is being called with, at which versions, and how far behind the latest published release each one is. Versions are observed from the `x-faable-client` header, so an integration that talks raw OIDC shows up only in the traffic breakdown.
1482
+ */
1483
+ get: operations["account/sdk-health"];
1484
+ put?: never;
1485
+ post?: never;
1486
+ delete?: never;
1487
+ options?: never;
1488
+ head?: never;
1489
+ patch?: never;
1490
+ trace?: never;
1491
+ };
1472
1492
  "/passwordless/verify_redirect": {
1473
1493
  parameters: {
1474
1494
  query?: never;
@@ -11889,6 +11909,55 @@ export interface operations {
11889
11909
  };
11890
11910
  };
11891
11911
  };
11912
+ "account/sdk-health": {
11913
+ parameters: {
11914
+ query?: never;
11915
+ header?: never;
11916
+ path: {
11917
+ account_id: string;
11918
+ };
11919
+ cookie?: never;
11920
+ };
11921
+ requestBody?: never;
11922
+ responses: {
11923
+ /** @description Default Response */
11924
+ 200: {
11925
+ headers: {
11926
+ [name: string]: unknown;
11927
+ };
11928
+ content: {
11929
+ "application/json": {
11930
+ libraries: {
11931
+ name: string;
11932
+ package: string;
11933
+ latest: string | null;
11934
+ installs: {
11935
+ version: string;
11936
+ sha: string | null;
11937
+ app: {
11938
+ client_id: string;
11939
+ name: string;
11940
+ } | null;
11941
+ lag: "current" | "patch" | "minor" | "major" | "unknown";
11942
+ legacy_unversioned: boolean;
11943
+ first_seen: string;
11944
+ last_seen: string;
11945
+ }[];
11946
+ }[];
11947
+ traffic: {
11948
+ days: number;
11949
+ total: number;
11950
+ by_class: {
11951
+ [key: string]: number;
11952
+ };
11953
+ } | null;
11954
+ latest_checked_at: string | null;
11955
+ retention_days: number;
11956
+ };
11957
+ };
11958
+ };
11959
+ };
11960
+ };
11892
11961
  "passwordless/verify_redirect": {
11893
11962
  parameters: {
11894
11963
  query: {
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from "./FaableAuthApi.js";
2
+ export { SDK_CLIENT, commit, version } from "./version.js";
2
3
  export * from "./api/generated-client.js";
3
4
  export * from "./api/api-types.js";
4
5
  export * from "./api/types.js";
5
- export { authClientCredentials, authApikey } from "@faable/sdk-base";
6
- export type { ApiParams, ClientCredentialsConfig, ApikeyConfig, AuthStrategy, AuthStrategyBuilder, AuthStrategyContext, } from "@faable/sdk-base";
6
+ export { authClientCredentials, authApikey, authBearer, authCookie, } from "@faable/sdk-base";
7
+ export type { ApiParams, ClientCredentialsConfig, ApikeyConfig, BearerConfig, RetryConfig, Fetcher, FetcherConfig, FetcherRequestParams, AuthStrategy, AuthStrategyBuilder, AuthStrategyContext, } from "@faable/sdk-base";
package/dist/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  export * from "./FaableAuthApi.js";
2
+ export { SDK_CLIENT, commit, version } from "./version.js";
2
3
  export * from "./api/generated-client.js";
3
4
  export * from "./api/api-types.js";
4
5
  export * from "./api/types.js";
5
6
  // Re-export the auth strategies (and their config/types) from
6
7
  // @faable/sdk-base so consumers depend on @faable/auth-sdk ONLY — sdk-base
7
8
  // stays a transitive dependency and no app needs to import it directly.
8
- export { authClientCredentials, authApikey } from "@faable/sdk-base";
9
+ export { authClientCredentials, authApikey, authBearer, authCookie, } from "@faable/sdk-base";
package/dist/version.d.ts CHANGED
@@ -1,2 +1,3 @@
1
- declare const version: string;
2
- export { version };
1
+ export declare const version = "0.0.0-dev";
2
+ export declare const commit = "0000000dev";
3
+ export declare const SDK_CLIENT = "auth-sdk/0.0.0-dev+0000000dev";
package/dist/version.js CHANGED
@@ -1,9 +1,23 @@
1
- import { createRequire } from "node:module";
2
- // Read the published package version at runtime. semantic-release bumps
3
- // package.json at publish time (after `npm run build`), so a build-time
4
- // constant would always be stale; reading it from the installed package
5
- // instead always reflects the real version. From dist/version.js,
6
- // "../package.json" resolves to the package root.
7
- const require = createRequire(import.meta.url);
8
- const { version } = require("../package.json");
9
- export { version };
1
+ // Both constants are replaced at release time by the
2
+ // `semantic-release-replace-plugin` entries in `.releaserc`, which rewrite
3
+ // every occurrence in `dist/*.js` with the version and commit being published.
4
+ // `tests/pack/verify-pack.mjs` fails the build if either sentinel is missing
5
+ // from the packed output, so a build can never ship anonymous.
6
+ //
7
+ // Why not read package.json at runtime: the previous `createRequire` from
8
+ // `node:module` kept this package out of every browser bundle — the hosted
9
+ // login pages could not import the SDK at all. Same pattern as auth-js.
10
+ //
11
+ // The sentinels MUST stay byte-identical to the `from` values in `.releaserc`.
12
+ export const version = "0.0.0-dev";
13
+ // Short git SHA of the released commit. The version dates a build; this names
14
+ // the exact tree, so a canonical log line or an audit entry leads straight to
15
+ // `git show <sha>`. Deliberately NOT hex: an unreleased build (dev, a local
16
+ // link) cannot be mistaken for a real commit — auth only records values that
17
+ // look like a SHA, and this one never will.
18
+ export const commit = "0000000dev";
19
+ // What this SDK writes in `x-faable-client`. Exported for a consumer that
20
+ // builds a strategy on its own (outside `FaableAuthApi`) and still wants the
21
+ // token request attributed — a bare `authClientCredentials` stamps only what
22
+ // it is given.
23
+ export const SDK_CLIENT = `auth-sdk/${version}+${commit}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faable/auth-sdk",
3
- "version": "2.5.95",
3
+ "version": "2.6.0",
4
4
  "author": "Marc Pomar <marc@faable.com>",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "type": "module",
12
12
  "dependencies": {
13
- "@faable/sdk-base": "^1.5.5"
13
+ "@faable/sdk-base": "^1.6.0"
14
14
  },
15
15
  "devDependencies": {
16
16
  "@ava/typescript": "^5.0.0",
@@ -18,9 +18,11 @@
18
18
  "@types/node": "^20.10.5",
19
19
  "ava": "^6.1.3",
20
20
  "dotenv": "^16.4.5",
21
+ "nock": "^13.5.5",
21
22
  "openapi-typescript": "^7.13.0",
22
23
  "rimraf": "^6.0.1",
23
24
  "semantic-release": "^25.0.3",
25
+ "semantic-release-replace-plugin": "^1.2.7",
24
26
  "tsimp": "^2.0.11",
25
27
  "typescript": "^5.3.3"
26
28
  },
@@ -30,7 +32,8 @@
30
32
  "prebuild": "npm run gentypes",
31
33
  "prepublishOnly": "npm run build",
32
34
  "test": "ava",
33
- "release": "semantic-release"
35
+ "release": "semantic-release",
36
+ "test:pack": "node tests/pack/verify-pack.mjs"
34
37
  },
35
38
  "resolutions": {
36
39
  "npm/chalk": "^4.1.2"
package/spec/openapi.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "info": {
4
4
  "title": "@faablecloud/auth",
5
5
  "description": "Auth Platform made by Faable. Manage Users and Roles",
6
- "version": "2.9.9",
6
+ "version": "2.10.0",
7
7
  "license": {
8
8
  "name": "private",
9
9
  "url": "https://faable.com/docs/platform/privacy-policy"
@@ -28137,6 +28137,218 @@
28137
28137
  }
28138
28138
  }
28139
28139
  },
28140
+ "/sdk-health/{account_id}": {
28141
+ "get": {
28142
+ "operationId": "account/sdk-health",
28143
+ "summary": "Library health for this account",
28144
+ "tags": [
28145
+ "account"
28146
+ ],
28147
+ "description": "Which Faable client libraries this account is being called with, at which versions, and how far behind the latest published release each one is. Versions are observed from the `x-faable-client` header, so an integration that talks raw OIDC shows up only in the traffic breakdown.",
28148
+ "parameters": [
28149
+ {
28150
+ "schema": {
28151
+ "type": "string"
28152
+ },
28153
+ "in": "path",
28154
+ "name": "account_id",
28155
+ "required": true
28156
+ }
28157
+ ],
28158
+ "security": [
28159
+ {
28160
+ "bearerAuth": []
28161
+ }
28162
+ ],
28163
+ "responses": {
28164
+ "200": {
28165
+ "description": "Default Response",
28166
+ "content": {
28167
+ "application/json": {
28168
+ "schema": {
28169
+ "type": "object",
28170
+ "required": [
28171
+ "libraries",
28172
+ "traffic",
28173
+ "latest_checked_at",
28174
+ "retention_days"
28175
+ ],
28176
+ "properties": {
28177
+ "libraries": {
28178
+ "type": "array",
28179
+ "items": {
28180
+ "type": "object",
28181
+ "required": [
28182
+ "name",
28183
+ "package",
28184
+ "latest",
28185
+ "installs"
28186
+ ],
28187
+ "properties": {
28188
+ "name": {
28189
+ "type": "string"
28190
+ },
28191
+ "package": {
28192
+ "type": "string"
28193
+ },
28194
+ "latest": {
28195
+ "anyOf": [
28196
+ {
28197
+ "type": "string"
28198
+ },
28199
+ {
28200
+ "type": "null"
28201
+ }
28202
+ ]
28203
+ },
28204
+ "installs": {
28205
+ "type": "array",
28206
+ "items": {
28207
+ "type": "object",
28208
+ "required": [
28209
+ "version",
28210
+ "sha",
28211
+ "app",
28212
+ "lag",
28213
+ "legacy_unversioned",
28214
+ "first_seen",
28215
+ "last_seen"
28216
+ ],
28217
+ "properties": {
28218
+ "version": {
28219
+ "type": "string"
28220
+ },
28221
+ "sha": {
28222
+ "anyOf": [
28223
+ {
28224
+ "type": "string"
28225
+ },
28226
+ {
28227
+ "type": "null"
28228
+ }
28229
+ ]
28230
+ },
28231
+ "app": {
28232
+ "anyOf": [
28233
+ {
28234
+ "type": "object",
28235
+ "required": [
28236
+ "client_id",
28237
+ "name"
28238
+ ],
28239
+ "properties": {
28240
+ "client_id": {
28241
+ "type": "string"
28242
+ },
28243
+ "name": {
28244
+ "type": "string"
28245
+ }
28246
+ }
28247
+ },
28248
+ {
28249
+ "type": "null"
28250
+ }
28251
+ ]
28252
+ },
28253
+ "lag": {
28254
+ "anyOf": [
28255
+ {
28256
+ "type": "string",
28257
+ "enum": [
28258
+ "current"
28259
+ ]
28260
+ },
28261
+ {
28262
+ "type": "string",
28263
+ "enum": [
28264
+ "patch"
28265
+ ]
28266
+ },
28267
+ {
28268
+ "type": "string",
28269
+ "enum": [
28270
+ "minor"
28271
+ ]
28272
+ },
28273
+ {
28274
+ "type": "string",
28275
+ "enum": [
28276
+ "major"
28277
+ ]
28278
+ },
28279
+ {
28280
+ "type": "string",
28281
+ "enum": [
28282
+ "unknown"
28283
+ ]
28284
+ }
28285
+ ]
28286
+ },
28287
+ "legacy_unversioned": {
28288
+ "type": "boolean"
28289
+ },
28290
+ "first_seen": {
28291
+ "type": "string"
28292
+ },
28293
+ "last_seen": {
28294
+ "type": "string"
28295
+ }
28296
+ }
28297
+ }
28298
+ }
28299
+ }
28300
+ }
28301
+ },
28302
+ "traffic": {
28303
+ "anyOf": [
28304
+ {
28305
+ "type": "object",
28306
+ "required": [
28307
+ "days",
28308
+ "total",
28309
+ "by_class"
28310
+ ],
28311
+ "properties": {
28312
+ "days": {
28313
+ "type": "number"
28314
+ },
28315
+ "total": {
28316
+ "type": "number"
28317
+ },
28318
+ "by_class": {
28319
+ "type": "object",
28320
+ "additionalProperties": {
28321
+ "type": "number"
28322
+ }
28323
+ }
28324
+ }
28325
+ },
28326
+ {
28327
+ "type": "null"
28328
+ }
28329
+ ]
28330
+ },
28331
+ "latest_checked_at": {
28332
+ "anyOf": [
28333
+ {
28334
+ "type": "string"
28335
+ },
28336
+ {
28337
+ "type": "null"
28338
+ }
28339
+ ]
28340
+ },
28341
+ "retention_days": {
28342
+ "type": "number"
28343
+ }
28344
+ }
28345
+ }
28346
+ }
28347
+ }
28348
+ }
28349
+ }
28350
+ }
28351
+ },
28140
28352
  "/passwordless/verify_redirect": {
28141
28353
  "get": {
28142
28354
  "operationId": "passwordless/verify_redirect",
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ // Guard on the PACKED build, run in CI between `npm run build` and the
3
+ // release. Three things that a green unit suite cannot see:
4
+ //
5
+ // 1. The version sentinels are in dist/ byte-identical to `.releaserc`, so
6
+ // semantic-release's replacement actually hits them. auth-js shipped as
7
+ // `auth-js/0.0.0` for months because its sentinel and its `.releaserc`
8
+ // disagreed — the rewrite hit a banner comment and nothing else.
9
+ // 2. Nothing in dist/ imports `node:*`. This package has to load in a browser
10
+ // bundle (the hosted login pages); a `createRequire` from `node:module`
11
+ // is exactly what kept it out until now.
12
+ // 3. The identity header on the wire: the default is `auth-sdk/<sentinels>`,
13
+ // and a consumer's `clientInfo.client` REPLACES it (the old static header
14
+ // used to override it silently).
15
+ import { execSync } from "node:child_process";
16
+ import { mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
17
+ import { tmpdir } from "node:os";
18
+ import { join, resolve } from "node:path";
19
+ import { fileURLToPath } from "node:url";
20
+
21
+ const ROOT = resolve(fileURLToPath(import.meta.url), "../../..");
22
+ const run = (cmd, opts = {}) => execSync(cmd, { stdio: "inherit", ...opts });
23
+ const capture = (cmd, opts = {}) => execSync(cmd, { encoding: "utf8", ...opts }).trim();
24
+ const fail = (msg, ...rest) => {
25
+ console.error("✖", msg, ...rest);
26
+ process.exit(1);
27
+ };
28
+
29
+ console.log("→ Building dist/");
30
+ run("npm run build", { cwd: ROOT });
31
+
32
+ // 1. Sentinels, verbatim, straight from .releaserc so the two cannot drift.
33
+ const releaserc = JSON.parse(readFileSync(join(ROOT, ".releaserc"), "utf8"));
34
+ const replacePlugin = releaserc.plugins.find(
35
+ (p) => Array.isArray(p) && p[0] === "semantic-release-replace-plugin",
36
+ );
37
+ if (!replacePlugin) fail(".releaserc has no semantic-release-replace-plugin entry");
38
+ const versionJs = readFileSync(join(ROOT, "dist/version.js"), "utf8");
39
+ for (const { from } of replacePlugin[1].replacements) {
40
+ if (!versionJs.includes(from)) {
41
+ fail(`dist/version.js does not contain the sentinel "${from}" that .releaserc replaces`);
42
+ }
43
+ }
44
+ console.log("✓ sentinels present in dist/version.js");
45
+
46
+ // 2. Browser-safety: no node: specifiers anywhere in the published JS.
47
+ const jsFiles = (dir) =>
48
+ readdirSync(dir, { withFileTypes: true }).flatMap((e) =>
49
+ e.isDirectory() ? jsFiles(join(dir, e.name)) : e.name.endsWith(".js") ? [join(dir, e.name)] : [],
50
+ );
51
+ for (const file of jsFiles(join(ROOT, "dist"))) {
52
+ const src = readFileSync(file, "utf8");
53
+ const hit = src.match(/from\s+["']node:[^"']+["']|require\(["']node:|createRequire\(/);
54
+ if (hit) fail(`${file.replace(ROOT + "/", "")} is not browser-safe: ${hit[0]}`);
55
+ }
56
+ console.log("✓ dist/ has no node: imports");
57
+
58
+ // 3. Pack, install into a clean consumer, look at the wire.
59
+ const tmp = mkdtempSync(join(tmpdir(), "faable-auth-sdk-pack-"));
60
+ try {
61
+ const tarball = join(ROOT, capture("npm pack --silent --pack-destination .", { cwd: ROOT }));
62
+ writeFileSync(
63
+ join(tmp, "package.json"),
64
+ JSON.stringify({ name: "consumer", version: "0.0.0", type: "module", private: true }, null, 2),
65
+ );
66
+ run(`npm install --silent --no-save "${tarball}"`, { cwd: tmp });
67
+ rmSync(tarball, { force: true });
68
+
69
+ const probe = `
70
+ const mod = await import("@faable/auth-sdk");
71
+ for (const name of ["FaableAuthApi", "authClientCredentials", "authApikey", "authBearer", "authCookie"]) {
72
+ if (typeof mod[name] === "undefined") { console.error("Missing export:", name); process.exit(1); }
73
+ }
74
+ const seen = [];
75
+ const adapter = async (config) => { seen.push(config); return { data: {}, status: 200, statusText: "OK", headers: {}, config }; };
76
+ const header = (i) => seen[i].headers["x-faable-client"] ?? seen[i].headers["X-Faable-Client"];
77
+
78
+ const dflt = mod.FaableAuthApi.create({ domain: "https://t.auth.faable.link", fetcher: { adapter } });
79
+ await dflt.fetcher.get("/me");
80
+ const expected = "auth-sdk/" + ${JSON.stringify(
81
+ replacePlugin[1].replacements.map((r) => r.from).join("+"),
82
+ )};
83
+ if (header(0) !== expected) { console.error("default x-faable-client must be", expected, "got", header(0)); process.exit(1); }
84
+ if (header(0).slice("auth-sdk/".length).length > 32) { console.error("version+sha exceeds auth's 32-char budget:", header(0)); process.exit(1); }
85
+
86
+ const wrapped = mod.FaableAuthApi.create({ domain: "https://t.auth.faable.link", fetcher: { adapter }, clientInfo: { client: "probe-ui/1.2.3" } });
87
+ await wrapped.fetcher.get("/me");
88
+ if (header(1) !== "probe-ui/1.2.3") { console.error("clientInfo.client must replace the default, got", header(1)); process.exit(1); }
89
+
90
+ const cookie = mod.FaableAuthApi.create({ domain: "https://t.auth.faable.link", fetcher: { adapter }, authStrategy: mod.authCookie });
91
+ await cookie.fetcher.get("/me");
92
+ if (seen[2].withCredentials !== true) { console.error("authCookie must send withCredentials"); process.exit(1); }
93
+ console.log("✓ wire identity: default", header(0), "| wrapped", header(1), "| cookie withCredentials");
94
+ `;
95
+ writeFileSync(join(tmp, "probe.mjs"), probe);
96
+ run("node probe.mjs", { cwd: tmp });
97
+ } finally {
98
+ rmSync(tmp, { recursive: true, force: true });
99
+ }
100
+ console.log("✓ pack verified");