@loopwise/admin-sdk 0.1.0-beta.3 → 0.1.0-beta.5

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/CHANGELOG.md CHANGED
@@ -1,6 +1,82 @@
1
- # @loopwise/admin-sdk
2
-
3
- ## 0.1.0-beta.3
1
+ # Changelog
2
+
3
+ All notable changes to `@loopwise/admin-sdk` are documented here.
4
+
5
+ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
+ Versioning: [SemVer](https://semver.org/) with pre-1.0 semantics — minor bumps
7
+ (`0.x → 0.x+1`) may include breaking changes; beta tags (`0.1.0-beta.N`)
8
+ follow the same rules as a single 0.x line.
9
+
10
+ ## [Unreleased]
11
+
12
+ ## [0.1.0-beta.5] — 2026-10-28
13
+
14
+ Moves the `/better-auth` integration to better-auth 1.7, which registers
15
+ `genericOAuth` providers as core social providers
16
+ ([better-auth#9069](https://github.com/better-auth/better-auth/pull/9069)).
17
+ better-auth 1.6 has no fix for
18
+ [GHSA-965c-763c-88jm](https://github.com/better-auth/better-auth/security/advisories/GHSA-965c-763c-88jm)
19
+ (fixed in 1.7.7), so 1.6 support is dropped. See
20
+ [TEACH-25069](https://linear.app/kaik/issue/TEACH-25069).
21
+
22
+ ### Added
23
+
24
+ - **`LoopwiseError#retryAfter`** — on a `RATE_LIMIT` error, the seconds from
25
+ the upstream `Retry-After` header (undefined when the header is absent or
26
+ not an integer). The Admin API now sends it both for request-count limits
27
+ and for the concurrency / execution-time limits introduced in
28
+ [TEACH-22231](https://linear.app/kaik/issue/TEACH-22231).
29
+
30
+ ### Changed
31
+
32
+ - **Breaking:** the optional `better-auth` peer dependency is now
33
+ `>=1.7.7` (was `>=1.6.11`). Upgrade `better-auth` to 1.7.7 or later.
34
+ - **Breaking:** `getLoopwiseRedirectURI()` returns
35
+ `<baseURL>/api/auth/callback/<providerId>` (was
36
+ `<baseURL>/api/auth/oauth2/callback/<providerId>`), the only callback
37
+ route better-auth 1.7 serves. Add the new URI to your OAuth client's
38
+ Redirect URIs at `/oauth/applications` before deploying, and remove the
39
+ old one once the upgrade is live.
40
+ - **Breaking (client):** start sign-in with
41
+ `authClient.signIn.social({ provider: 'loopwise' })`; better-auth 1.7
42
+ removed `signIn.oauth2`. On the server, `auth.api.getAccessToken()` now
43
+ selects the account by `accountId` rather than `providerId` — the README
44
+ shows the `listUserAccounts()` lookup.
45
+
46
+ ### Fixed
47
+
48
+ - **Accounts are keyed by the OIDC `sub` claim** whatever the discovery
49
+ document advertises. Without this, better-auth 1.7 reads the profile's
50
+ `id` field when discovery lists no id_token signing algorithm; Loopwise
51
+ userinfo has no `id`, so sign-in fails.
52
+
53
+ ## [0.1.0-beta.4] — 2026-06-01
54
+
55
+ Docs-only release — surface the OIDC extension claims shipped in
56
+ [TEACH-18913](https://linear.app/kaik/issue/TEACH-18913) on the
57
+ npm-rendered README so integrators see the canonical `mapProfileToUser`
58
+ pattern without leaving the package page. No code change.
59
+
60
+ ### Documentation
61
+
62
+ - **README `mapProfileToUser` example** — replaces the
63
+ `// pending TEACH-18913` placeholder with the shipped pattern that
64
+ reads `profile.org_id` (school identifier) + `profile.roles[0]` (the
65
+ highest-privilege Loopwise role) through a `ROLE_MAP` lookup, with
66
+ strict-TS-safe optional handling for the no-school-context case.
67
+ - **Edge cases documented inline** — both `org_id` and `roles` are
68
+ omitted from the userinfo response when the token has no school
69
+ context; `roles` can additionally be `[]` for a school-bound token
70
+ whose user has no roles on that school.
71
+ - **Cross-links to docs.loopwise.com** — points at the new
72
+ [AI Agent Integration guide](https://docs.loopwise.com/guides/agent-integration)
73
+ for the full cookbook (multi-school fallback via `account:read`,
74
+ role-change refresh, common mistakes).
75
+
76
+ See [TEACH-18914](https://linear.app/kaik/issue/TEACH-18914) for the
77
+ docs PR this release surfaces.
78
+
79
+ ## [0.1.0-beta.3] — 2026-05-22
4
80
 
5
81
  DX quick wins informed by the first real integration ([nii-course-system](https://github.com/niischool-tw/nii-course-system/pull/38),
6
82
  agent-driven, shipped against `0.1.0-beta.0`). See
@@ -8,17 +84,7 @@ agent-driven, shipped against `0.1.0-beta.0`). See
8
84
  feedback report; [TEACH-18912](https://linear.app/kaik/issue/TEACH-18912)
9
85
  for this PR.
10
86
 
11
- ### Breaking changes
12
-
13
- - **`baseUrl` → `baseURL`** on both `LoopwiseAuthOptions` (better-auth
14
- wrapper) and `LoopwiseAdminConfig` (core admin client). Casing now
15
- matches better-auth's own `baseURL` / `BETTER_AUTH_URL` convention,
16
- removing a same-module typo footgun. Migration is a one-character
17
- rename at any call site that passes the option explicitly; TS
18
- catches it at compile time. Callers relying on the default are
19
- unaffected.
20
-
21
- ### Features
87
+ ### Added
22
88
 
23
89
  - **`mapProfileToUser` pass-through on `loopwise({})`** — populate
24
90
  custom `User` columns at sign-up (e.g. `teachifyUserId: profile.sub`)
@@ -29,8 +95,15 @@ for this PR.
29
95
  URI to whitelist on your OAuth client — the better-auth callback
30
96
  path includes a `/oauth2/` segment that's easy to miss otherwise.
31
97
 
32
- ### Behavior changes
98
+ ### Changed
33
99
 
100
+ - **Breaking:** `baseUrl` → `baseURL` on both `LoopwiseAuthOptions`
101
+ (better-auth wrapper) and `LoopwiseAdminConfig` (core admin client).
102
+ Casing now matches better-auth's own `baseURL` / `BETTER_AUTH_URL`
103
+ convention, removing a same-module typo footgun. Migration is a
104
+ one-character rename at any call site that passes the option
105
+ explicitly; TS catches it at compile time. Callers relying on the
106
+ default are unaffected.
34
107
  - **`org_admin` default scope shrunk to `['openid', 'profile', 'email']`**
35
108
  (was `['openid', 'profile', 'email', 'courses:read', 'members:read']`).
36
109
  New OAuth clients don't have API scopes enabled by default, so the
@@ -39,34 +112,36 @@ for this PR.
39
112
  `scopes` explicitly AND enable matching scopes on their OAuth client
40
113
  in the Loopwise admin UI.
41
114
 
42
- ### Docs
115
+ ### Documentation
43
116
 
44
117
  - README adds Redirect URI, Scopes, Profile-mapping, and Roadmap
45
118
  sections informed by the same feedback report.
46
119
 
47
- ## 0.1.0-beta.2
120
+ ## [0.1.0-beta.2] — 2026-05-21
48
121
 
49
- Add second typed resource:
122
+ ### Added
50
123
 
51
124
  - `admin.members.list({ perPage, page, role })` returns `AdminMemberPage`
52
- (mirrors `courses.list` shape).
125
+ (mirrors `courses.list` shape) — second typed resource.
53
126
  - `AdminMember`: `id`, `name`, `email`, `phoneNumber`, `createdAt`,
54
127
  `lastSignInAt`.
55
128
  - `AdminMemberRole`: `'student' | 'teaching_assistant'` mirrors the
56
129
  schema enum.
57
130
  - Requires OAuth scope `members:read`.
58
131
 
59
- ## 0.1.0-beta.1
132
+ ## [0.1.0-beta.1] — 2026-05-21
60
133
 
61
134
  No functional changes. Validates the tag-triggered OIDC publish path
62
135
  now that the npm Trusted Publisher entry is configured (the first
63
136
  publish — `0.1.0-beta.0` — had to be done manually because npm OIDC
64
137
  can't create new package names).
65
138
 
66
- ## 0.1.0-beta.0
139
+ ## [0.1.0-beta.0] — 2026-05-21
67
140
 
68
141
  First public pre-release.
69
142
 
143
+ ### Added
144
+
70
145
  - `createAdminClient({ accessToken, refreshAccessToken?, baseUrl? })` returns a
71
146
  `LoopwiseAdmin` interface with `.graphql<TData, TVars>()` typed pass-through
72
147
  against `/admin/graphql`.
@@ -81,3 +156,11 @@ First public pre-release.
81
156
  Server Metadata discovery URL (`/.well-known/oauth-authorization-server`),
82
157
  audience-aware providerId (`loopwise` / `loopwise-member`) and scope
83
158
  defaults.
159
+
160
+ [Unreleased]: https://github.com/kaikhq/manekineko/compare/@loopwise/admin-sdk@0.1.0-beta.5...HEAD
161
+ [0.1.0-beta.5]: https://github.com/kaikhq/manekineko/compare/@loopwise/admin-sdk@0.1.0-beta.4...@loopwise/admin-sdk@0.1.0-beta.5
162
+ [0.1.0-beta.4]: https://github.com/kaikhq/manekineko/compare/@loopwise/admin-sdk@0.1.0-beta.3...@loopwise/admin-sdk@0.1.0-beta.4
163
+ [0.1.0-beta.3]: https://github.com/kaikhq/manekineko/compare/@loopwise/admin-sdk@0.1.0-beta.2...@loopwise/admin-sdk@0.1.0-beta.3
164
+ [0.1.0-beta.2]: https://github.com/kaikhq/manekineko/compare/@loopwise/admin-sdk@0.1.0-beta.1...@loopwise/admin-sdk@0.1.0-beta.2
165
+ [0.1.0-beta.1]: https://github.com/kaikhq/manekineko/compare/@loopwise/admin-sdk@0.1.0-beta.0...@loopwise/admin-sdk@0.1.0-beta.1
166
+ [0.1.0-beta.0]: https://github.com/kaikhq/manekineko/releases/tag/@loopwise/admin-sdk@0.1.0-beta.0
package/README.md CHANGED
@@ -20,6 +20,8 @@ const { courses } = await admin.graphql<{ courses: { nodes: { id: string; name:
20
20
 
21
21
  ## With better-auth (Next.js)
22
22
 
23
+ Requires `better-auth` 1.7.7 or later.
24
+
23
25
  ```ts
24
26
  // auth.ts
25
27
  import { betterAuth } from 'better-auth';
@@ -43,9 +45,14 @@ import { createAdminClient } from '@loopwise/admin-sdk';
43
45
  import { auth } from '@/lib/auth';
44
46
 
45
47
  export default async function Page() {
48
+ const requestHeaders = await headers();
49
+ const accounts = await auth.api.listUserAccounts({ headers: requestHeaders });
50
+ const loopwiseAccount = accounts.find((a) => a.providerId === 'loopwise');
51
+ if (!loopwiseAccount) throw new Error('Not signed in with Loopwise');
52
+
46
53
  const { accessToken } = await auth.api.getAccessToken({
47
- body: { providerId: 'loopwise' },
48
- headers: headers(),
54
+ body: { accountId: loopwiseAccount.id },
55
+ headers: requestHeaders,
49
56
  });
50
57
  const admin = createAdminClient({ accessToken });
51
58
  const { courses } = await admin.graphql<{ courses: { nodes: { id: string; name: string }[] } }>({
@@ -57,10 +64,8 @@ export default async function Page() {
57
64
 
58
65
  ### Redirect URI
59
66
 
60
- Better Auth's catch-all handler is mounted at `/api/auth/[...all]`, but the
61
- `genericOAuth` plugin's callback path is `/oauth2/callback/<providerId>` —
62
- note the `oauth2/` segment (different from many OAuth tutorials that show
63
- `/api/auth/callback/...`).
67
+ Better Auth's catch-all handler is mounted at `/api/auth/[...all]`, and
68
+ the Loopwise provider's callback is `/api/auth/callback/<providerId>`.
64
69
 
65
70
  Use the exported helper to compute the exact URI to whitelist on your
66
71
  OAuth client:
@@ -71,12 +76,27 @@ import { getLoopwiseRedirectURI } from '@loopwise/admin-sdk/better-auth';
71
76
  console.log(
72
77
  getLoopwiseRedirectURI({ baseURL: process.env.BETTER_AUTH_URL! }),
73
78
  );
74
- // dev: http://localhost:3000/api/auth/oauth2/callback/loopwise
75
- // prod: https://your.app/api/auth/oauth2/callback/loopwise
79
+ // dev: http://localhost:3000/api/auth/callback/loopwise
80
+ // prod: https://your.app/api/auth/callback/loopwise
76
81
  ```
77
82
 
78
83
  For multi-audience apps, pass `audience: 'member'` to get
79
- `/oauth2/callback/loopwise-member`.
84
+ `/api/auth/callback/loopwise-member`.
85
+
86
+ ### Signing in from the client
87
+
88
+ The Loopwise provider is a regular better-auth social provider, so the
89
+ client starts the flow with `signIn.social`:
90
+
91
+ ```ts
92
+ import { createAuthClient } from 'better-auth/react';
93
+
94
+ const authClient = createAuthClient();
95
+
96
+ await authClient.signIn.social({ provider: 'loopwise', callbackURL: '/courses' });
97
+ ```
98
+
99
+ Use `provider: 'loopwise-member'` for the member audience.
80
100
 
81
101
  ### Scopes
82
102
 
@@ -101,21 +121,46 @@ Mismatch between requested and enabled scopes surfaces as
101
121
  ### Mapping OAuth claims to your user table
102
122
 
103
123
  Use `mapProfileToUser` to populate custom columns (declared via
104
- better-auth's `additionalFields`) at sign-up:
124
+ better-auth's `additionalFields`) at sign-up. The `profile` is whatever
125
+ `/api/oauth/userinfo` returns — standard OIDC claims (`sub`, `name`,
126
+ `email`, `email_verified`) plus two Loopwise extension claims:
127
+
128
+ - `org_id` — the school the access token is bound to. Both `org_id`
129
+ and `roles` are **omitted from the response entirely** when the
130
+ token has no school context (e.g. a platform-managed personal-
131
+ access flow that hasn't picked a school yet).
132
+ - `roles` — the user's roles on that school, ordered by privilege; may
133
+ be an empty array when the token *is* school-bound but the user has
134
+ no roles on that school. When non-empty, `roles[0]` is the
135
+ highest-privilege role and is the recommended source for
136
+ onboarding-time role mapping.
105
137
 
106
138
  ```ts
139
+ const ROLE_MAP: Record<string, string> = {
140
+ owner: 'admin',
141
+ manager: 'admin',
142
+ admin: 'admin',
143
+ teacher: 'instructor',
144
+ teaching_assistant: 'instructor',
145
+ student: 'member',
146
+ };
147
+
107
148
  loopwise({
108
149
  clientId, clientSecret,
109
- mapProfileToUser: (profile) => ({
110
- teachifyUserId: profile.sub,
111
- // role: profile['loopwise:org_role'], // pending TEACH-18913
112
- }),
150
+ mapProfileToUser: (profile) => {
151
+ const primary = (profile.roles as string[] | undefined)?.[0];
152
+ return {
153
+ teachifyUserId: profile.sub as string,
154
+ schoolId: profile.org_id as string | undefined,
155
+ role: primary ? (ROLE_MAP[primary] ?? 'member') : 'member',
156
+ };
157
+ },
113
158
  })
114
159
  ```
115
160
 
116
- The `profile` is whatever `/oauth/userinfo` returns — standard OIDC
117
- claims today, plus Loopwise-specific `loopwise:*` claims once
118
- [TEACH-18913](https://linear.app/kaik/issue/TEACH-18913) ships.
161
+ For the full claim schema, role ordering contract, and edge cases
162
+ (multi-school users, platform-managed tokens, role-change refresh),
163
+ see the [AI Agent Integration guide](https://docs.loopwise.com/guides/agent-integration).
119
164
 
120
165
  ## Typed resources roadmap
121
166
 
@@ -1,7 +1,6 @@
1
1
  import { BetterAuthPlugin } from "better-auth/types";
2
-
3
2
  //#region src/better-auth.d.ts
4
- type LoopwiseAuthAudience = 'org_admin' | 'member';
3
+ type LoopwiseAuthAudience = "org_admin" | "member";
5
4
  interface LoopwiseAuthOptions {
6
5
  /** OAuth client ID issued at `/oauth/applications` in the school admin UI. */
7
6
  clientId: string;
@@ -81,21 +80,20 @@ interface GetRedirectURIOptions {
81
80
  }
82
81
  /**
83
82
  * Compute the exact redirect URI you need to register on your Loopwise
84
- * OAuth client. Better Auth's `genericOAuth` plugin uses the path
85
- * `/api/auth/oauth2/callback/<providerId>` — note the `oauth2/` segment
86
- * (different from many OAuth tutorials that show `/api/auth/callback/...`).
83
+ * OAuth client: better-auth's callback route,
84
+ * `/api/auth/callback/<providerId>`, under your app's `baseURL`.
87
85
  *
88
86
  * @example
89
87
  * // dev
90
88
  * getLoopwiseRedirectURI({ baseURL: 'http://localhost:3000' });
91
- * // → 'http://localhost:3000/api/auth/oauth2/callback/loopwise'
89
+ * // → 'http://localhost:3000/api/auth/callback/loopwise'
92
90
  *
93
91
  * // prod, member audience
94
92
  * getLoopwiseRedirectURI({
95
93
  * baseURL: 'https://app.example.com',
96
94
  * audience: 'member',
97
95
  * });
98
- * // → 'https://app.example.com/api/auth/oauth2/callback/loopwise-member'
96
+ * // → 'https://app.example.com/api/auth/callback/loopwise-member'
99
97
  *
100
98
  * Use this at app boot to print the URI so you (or your agent) can paste
101
99
  * it straight into the OAuth client's redirect-URI list.
@@ -1,7 +1,7 @@
1
1
  import { genericOAuth } from "better-auth/plugins";
2
2
  //#region src/better-auth.ts
3
3
  const DEFAULT_BASE_URL = "https://app.loopwise.com";
4
- const BETTER_AUTH_OAUTH2_CALLBACK_PATH = "/api/auth/oauth2/callback";
4
+ const BETTER_AUTH_CALLBACK_PATH = "/api/auth/callback";
5
5
  const AUDIENCE_PROFILES = {
6
6
  org_admin: {
7
7
  providerId: "loopwise",
@@ -22,28 +22,27 @@ const AUDIENCE_PROFILES = {
22
22
  };
23
23
  /**
24
24
  * Compute the exact redirect URI you need to register on your Loopwise
25
- * OAuth client. Better Auth's `genericOAuth` plugin uses the path
26
- * `/api/auth/oauth2/callback/<providerId>` — note the `oauth2/` segment
27
- * (different from many OAuth tutorials that show `/api/auth/callback/...`).
25
+ * OAuth client: better-auth's callback route,
26
+ * `/api/auth/callback/<providerId>`, under your app's `baseURL`.
28
27
  *
29
28
  * @example
30
29
  * // dev
31
30
  * getLoopwiseRedirectURI({ baseURL: 'http://localhost:3000' });
32
- * // → 'http://localhost:3000/api/auth/oauth2/callback/loopwise'
31
+ * // → 'http://localhost:3000/api/auth/callback/loopwise'
33
32
  *
34
33
  * // prod, member audience
35
34
  * getLoopwiseRedirectURI({
36
35
  * baseURL: 'https://app.example.com',
37
36
  * audience: 'member',
38
37
  * });
39
- * // → 'https://app.example.com/api/auth/oauth2/callback/loopwise-member'
38
+ * // → 'https://app.example.com/api/auth/callback/loopwise-member'
40
39
  *
41
40
  * Use this at app boot to print the URI so you (or your agent) can paste
42
41
  * it straight into the OAuth client's redirect-URI list.
43
42
  */
44
43
  function getLoopwiseRedirectURI(options) {
45
44
  const profile = AUDIENCE_PROFILES[options.audience ?? "org_admin"];
46
- return `${options.baseURL.replace(/\/$/, "")}${BETTER_AUTH_OAUTH2_CALLBACK_PATH}/${profile.providerId}`;
45
+ return `${options.baseURL.replace(/\/$/, "")}${BETTER_AUTH_CALLBACK_PATH}/${profile.providerId}`;
47
46
  }
48
47
  function loopwise(options) {
49
48
  const profile = AUDIENCE_PROFILES[options.audience ?? "org_admin"];
@@ -56,6 +55,7 @@ function loopwise(options) {
56
55
  pkce: true,
57
56
  accessType: "offline",
58
57
  scopes: options.scopes ?? [...profile.defaultScopes],
58
+ accountSubject: ({ profile: userInfo }) => userInfo.sub ?? "",
59
59
  mapProfileToUser: options.mapProfileToUser
60
60
  }] });
61
61
  }
package/dist/index.d.mts CHANGED
@@ -22,8 +22,11 @@ interface LoopwiseAdminConfig {
22
22
  *
23
23
  * Typical wiring (with better-auth):
24
24
  * refreshAccessToken: async () => {
25
+ * const accounts = await auth.api.listUserAccounts({ headers: req.headers });
26
+ * const account = accounts.find((a) => a.providerId === 'loopwise');
27
+ * if (!account) throw new Error('Not signed in with Loopwise');
25
28
  * const { accessToken } = await auth.api.getAccessToken({
26
- * body: { providerId: 'loopwise' }, headers: req.headers,
29
+ * body: { accountId: account.id }, headers: req.headers,
27
30
  * });
28
31
  * return accessToken;
29
32
  * }
@@ -79,7 +82,7 @@ declare class LoopwiseAdminClient {
79
82
  }
80
83
  //#endregion
81
84
  //#region src/error.d.ts
82
- type LoopwiseErrorCode = 'NETWORK' | 'AUTH' | 'GRAPHQL' | 'RATE_LIMIT' | 'CONFIG';
85
+ type LoopwiseErrorCode = "NETWORK" | "AUTH" | "GRAPHQL" | "RATE_LIMIT" | "CONFIG";
83
86
  interface GraphQLError {
84
87
  message: string;
85
88
  path?: (string | number)[];
@@ -91,11 +94,14 @@ declare class LoopwiseError extends Error {
91
94
  readonly status?: number;
92
95
  readonly graphqlErrors?: GraphQLError[];
93
96
  readonly requestId?: string;
97
+ /** Seconds to wait before retrying, from the upstream `Retry-After`; only on `RATE_LIMIT`. */
98
+ readonly retryAfter?: number;
94
99
  constructor(code: LoopwiseErrorCode, message: string, detail?: {
95
100
  endpoint?: string;
96
101
  status?: number;
97
102
  graphqlErrors?: GraphQLError[];
98
103
  requestId?: string;
104
+ retryAfter?: number;
99
105
  });
100
106
  }
101
107
  //#endregion
@@ -165,7 +171,7 @@ interface AdminMemberPage {
165
171
  hasNextPage: boolean;
166
172
  hasPreviousPage: boolean;
167
173
  }
168
- type AdminMemberRole = 'student' | 'teaching_assistant';
174
+ type AdminMemberRole = "student" | "teaching_assistant";
169
175
  interface MembersListArgs {
170
176
  /** Items per page. Schema default 20, max 50. */
171
177
  perPage?: number;
package/dist/index.mjs CHANGED
@@ -5,6 +5,8 @@ var LoopwiseError = class extends Error {
5
5
  status;
6
6
  graphqlErrors;
7
7
  requestId;
8
+ /** Seconds to wait before retrying, from the upstream `Retry-After`; only on `RATE_LIMIT`. */
9
+ retryAfter;
8
10
  constructor(code, message, detail = {}) {
9
11
  super(message);
10
12
  this.name = "LoopwiseError";
@@ -13,6 +15,7 @@ var LoopwiseError = class extends Error {
13
15
  this.status = detail.status;
14
16
  this.graphqlErrors = detail.graphqlErrors;
15
17
  this.requestId = detail.requestId;
18
+ this.retryAfter = detail.retryAfter;
16
19
  }
17
20
  };
18
21
  //#endregion
@@ -110,11 +113,16 @@ var LoopwiseAdminClient = class {
110
113
  status: 401,
111
114
  requestId
112
115
  });
113
- if (response.status === 429) throw new LoopwiseError("RATE_LIMIT", "Rate limit exceeded", {
114
- endpoint: this.endpoint,
115
- status: 429,
116
- requestId
117
- });
116
+ if (response.status === 429) {
117
+ const retryAfterHeader = response.headers.get("retry-after");
118
+ const retryAfter = retryAfterHeader !== null && /^\d+$/.test(retryAfterHeader) ? Number(retryAfterHeader) : void 0;
119
+ throw new LoopwiseError("RATE_LIMIT", retryAfter === void 0 ? "Rate limit exceeded" : `Rate limit exceeded; retry after ${retryAfter}s`, {
120
+ endpoint: this.endpoint,
121
+ status: 429,
122
+ requestId,
123
+ retryAfter
124
+ });
125
+ }
118
126
  if (!response.ok) throw new LoopwiseError("NETWORK", `Upstream returned HTTP ${response.status}`, {
119
127
  endpoint: this.endpoint,
120
128
  status: response.status,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loopwise/admin-sdk",
3
- "version": "0.1.0-beta.3",
3
+ "version": "0.1.0-beta.5",
4
4
  "description": "Loopwise admin GraphQL SDK — typed access to /admin/graphql for tenant-built applications",
5
5
  "repository": {
6
6
  "type": "git",
@@ -47,7 +47,7 @@
47
47
  "test": "vitest run"
48
48
  },
49
49
  "peerDependencies": {
50
- "better-auth": ">=1.6.11"
50
+ "better-auth": ">=1.7.7"
51
51
  },
52
52
  "peerDependenciesMeta": {
53
53
  "better-auth": {
@@ -55,11 +55,11 @@
55
55
  }
56
56
  },
57
57
  "devDependencies": {
58
- "better-auth": "1.6.11",
58
+ "better-auth": "1.7.14",
59
59
  "tsconfig": "workspace:*",
60
- "tsdown": "0.22.0",
61
- "typescript": "5.9.3",
62
- "vitest": "4.1.0"
60
+ "tsdown": "catalog:",
61
+ "typescript": "catalog:typescript7",
62
+ "vitest": "catalog:"
63
63
  },
64
64
  "publishConfig": {
65
65
  "access": "public"