@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 +104 -21
- package/README.md +62 -17
- package/dist/better-auth.d.mts +5 -7
- package/dist/better-auth.mjs +7 -7
- package/dist/index.d.mts +9 -3
- package/dist/index.mjs +13 -5
- package/package.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,82 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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
|
-
|
|
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: {
|
|
48
|
-
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]`,
|
|
61
|
-
|
|
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/
|
|
75
|
-
// prod: https://your.app/api/auth/
|
|
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
|
-
`/
|
|
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
|
-
|
|
111
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
[
|
|
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
|
|
package/dist/better-auth.d.mts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { BetterAuthPlugin } from "better-auth/types";
|
|
2
|
-
|
|
3
2
|
//#region src/better-auth.d.ts
|
|
4
|
-
type LoopwiseAuthAudience =
|
|
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
|
|
85
|
-
* `/api/auth/
|
|
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/
|
|
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/
|
|
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.
|
package/dist/better-auth.mjs
CHANGED
|
@@ -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
|
|
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
|
|
26
|
-
* `/api/auth/
|
|
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/
|
|
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/
|
|
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(/\/$/, "")}${
|
|
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: {
|
|
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 =
|
|
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 =
|
|
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)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
+
"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.
|
|
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.
|
|
58
|
+
"better-auth": "1.7.14",
|
|
59
59
|
"tsconfig": "workspace:*",
|
|
60
|
-
"tsdown": "
|
|
61
|
-
"typescript": "
|
|
62
|
-
"vitest": "
|
|
60
|
+
"tsdown": "catalog:",
|
|
61
|
+
"typescript": "catalog:typescript7",
|
|
62
|
+
"vitest": "catalog:"
|
|
63
63
|
},
|
|
64
64
|
"publishConfig": {
|
|
65
65
|
"access": "public"
|