@~lyre/auth 0.1.1 → 0.3.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 +1 -1
- package/dist/admin.d.ts +146 -0
- package/dist/admin.js +193 -0
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ and identity handling (HMAC-signed cookies, the accounts login → callback →
|
|
|
5
5
|
logout exchange, tenant resolution), plus an optional turnkey **SvelteKit**
|
|
6
6
|
adapter that wires it all up in a few lines.
|
|
7
7
|
|
|
8
|
-
Ships
|
|
8
|
+
Ships built ESM output from `dist/`, with type declarations.
|
|
9
9
|
|
|
10
10
|
## Install
|
|
11
11
|
|
package/dist/admin.d.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
interface AccountsAdminOptions {
|
|
2
|
+
/** Accounts base URL, e.g. `https://accounts.example.com`. */
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
/** A user session id (bearer) for session-authed endpoints (invites, profile update, user list). */
|
|
5
|
+
sessionToken?: string;
|
|
6
|
+
/** A machine service key (`sk_…`) for `x-api-key`-authed reads (group members). */
|
|
7
|
+
serviceKey?: string;
|
|
8
|
+
/** Override the fetch implementation (defaults to the global). */
|
|
9
|
+
fetchImpl?: typeof fetch;
|
|
10
|
+
}
|
|
11
|
+
interface AccountsUser {
|
|
12
|
+
id: string;
|
|
13
|
+
email: string;
|
|
14
|
+
name?: string | null;
|
|
15
|
+
firstName?: string | null;
|
|
16
|
+
lastName?: string | null;
|
|
17
|
+
emailVerified?: boolean;
|
|
18
|
+
/** The user's avatar URL (from their latest OAuth sign-in with a picture, or their upload). */
|
|
19
|
+
avatarUrl?: string | null;
|
|
20
|
+
profileData?: Record<string, string>;
|
|
21
|
+
userGroups?: Array<{
|
|
22
|
+
id: string;
|
|
23
|
+
slug?: string;
|
|
24
|
+
displayName?: string;
|
|
25
|
+
role?: string;
|
|
26
|
+
}>;
|
|
27
|
+
apps?: Array<{
|
|
28
|
+
id: string;
|
|
29
|
+
slug: string;
|
|
30
|
+
name?: string;
|
|
31
|
+
}>;
|
|
32
|
+
[key: string]: unknown;
|
|
33
|
+
}
|
|
34
|
+
interface ChangePasswordResult {
|
|
35
|
+
success: boolean;
|
|
36
|
+
mode?: 'set' | 'changed';
|
|
37
|
+
revokedCount?: number;
|
|
38
|
+
error?: string;
|
|
39
|
+
}
|
|
40
|
+
interface ListUsersQuery {
|
|
41
|
+
tenantId?: string;
|
|
42
|
+
appId?: string;
|
|
43
|
+
q?: string;
|
|
44
|
+
name?: string;
|
|
45
|
+
email?: string;
|
|
46
|
+
limit?: number;
|
|
47
|
+
offset?: number;
|
|
48
|
+
}
|
|
49
|
+
interface ListUsersResult {
|
|
50
|
+
users: AccountsUser[];
|
|
51
|
+
pagination?: {
|
|
52
|
+
total: number;
|
|
53
|
+
limit: number;
|
|
54
|
+
offset: number;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
interface AppMember {
|
|
58
|
+
id: string;
|
|
59
|
+
email: string;
|
|
60
|
+
name?: string | null;
|
|
61
|
+
via?: 'member' | 'role';
|
|
62
|
+
}
|
|
63
|
+
interface InviteResult {
|
|
64
|
+
/** `added` = the email already had an Accounts user and was granted app access immediately;
|
|
65
|
+
* `invited` = a new invite was created and an accept link emailed. */
|
|
66
|
+
status: 'added' | 'invited';
|
|
67
|
+
[key: string]: unknown;
|
|
68
|
+
}
|
|
69
|
+
interface GroupMember {
|
|
70
|
+
id: string;
|
|
71
|
+
email: string;
|
|
72
|
+
name?: string | null;
|
|
73
|
+
firstName?: string | null;
|
|
74
|
+
lastName?: string | null;
|
|
75
|
+
role?: string | null;
|
|
76
|
+
}
|
|
77
|
+
/** Thrown on a non-2xx Accounts response so callers can surface the upstream message + status. */
|
|
78
|
+
declare class AccountsAdminError extends Error {
|
|
79
|
+
readonly status: number;
|
|
80
|
+
readonly body?: unknown | undefined;
|
|
81
|
+
constructor(status: number, message: string, body?: unknown | undefined);
|
|
82
|
+
}
|
|
83
|
+
declare class AccountsAdmin {
|
|
84
|
+
private readonly opts;
|
|
85
|
+
private readonly base;
|
|
86
|
+
private readonly f;
|
|
87
|
+
constructor(opts: AccountsAdminOptions);
|
|
88
|
+
/** List/search users in a tenant. Session-authed (caller must be privileged in the tenant). */
|
|
89
|
+
listUsers(query?: ListUsersQuery): Promise<ListUsersResult>;
|
|
90
|
+
/** One user's full detail (includes `profileData`, `userGroups`, `apps`). Session-authed. */
|
|
91
|
+
getUser(userId: string): Promise<AccountsUser>;
|
|
92
|
+
/** Update a user (self, or admin/owner/super-admin). `profileData` requires `appId`. Session-authed.
|
|
93
|
+
* Password is NOT set here — use {@link changePassword} (Accounts verifies the current password). */
|
|
94
|
+
updateUser(userId: string, input: {
|
|
95
|
+
name?: string;
|
|
96
|
+
email?: string;
|
|
97
|
+
appId?: string;
|
|
98
|
+
profileData?: Record<string, string>;
|
|
99
|
+
}): Promise<AccountsUser>;
|
|
100
|
+
/** Change the signed-in user's password. Session-authed; Accounts verifies `currentPassword` and,
|
|
101
|
+
* with `revokeOtherSessions`, logs the user out everywhere else. The target is the session's own
|
|
102
|
+
* user — no id is passed. */
|
|
103
|
+
changePassword(input: {
|
|
104
|
+
currentPassword: string;
|
|
105
|
+
newPassword: string;
|
|
106
|
+
confirmPassword?: string;
|
|
107
|
+
revokeOtherSessions?: boolean;
|
|
108
|
+
}): Promise<ChangePasswordResult>;
|
|
109
|
+
/** Invite an email to an app: adds an existing Accounts user immediately, else emails an accept
|
|
110
|
+
* link. Session-authed (caller must have access to the app). */
|
|
111
|
+
inviteToApp(appSlug: string, input: {
|
|
112
|
+
email: string;
|
|
113
|
+
}): Promise<InviteResult>;
|
|
114
|
+
/** Everyone with access to an app. Session-authed. */
|
|
115
|
+
listAppUsers(appSlug: string): Promise<AppMember[]>;
|
|
116
|
+
/** Revoke a user's access to an app. Session-authed. */
|
|
117
|
+
removeAppUser(appSlug: string, userId: string): Promise<void>;
|
|
118
|
+
/** Members of a user_group (the workspace's agents). Service-key authed (`stats:read`). */
|
|
119
|
+
listGroupMembers(groupId: string, opts?: {
|
|
120
|
+
appId?: string;
|
|
121
|
+
}): Promise<GroupMember[]>;
|
|
122
|
+
/** Read a user's app-scoped `profileData` (the store for per-app preferences). Session-authed. */
|
|
123
|
+
getAppProfile(userId: string): Promise<Record<string, string>>;
|
|
124
|
+
/** Write app-scoped `profileData` fields for a user (e.g. availability, soundEnabled). All values
|
|
125
|
+
* are stored as strings by Accounts; booleans are serialised. Session-authed. */
|
|
126
|
+
setAppProfile(userId: string, appId: string, profile: Record<string, string | number | boolean>): Promise<AccountsUser>;
|
|
127
|
+
/** Upload a user's avatar image. The bytes are stored by Accounts and a public URL is returned +
|
|
128
|
+
* persisted (source = 'upload', which survives later OAuth sign-ins). Session-authed, self-only.
|
|
129
|
+
* `image` is the file bytes (Blob/Buffer/Uint8Array); `mimeType` + `filename` describe it. */
|
|
130
|
+
uploadAvatar(userId: string, image: Blob | ArrayBuffer | Uint8Array, opts?: {
|
|
131
|
+
mimeType?: string;
|
|
132
|
+
filename?: string;
|
|
133
|
+
}): Promise<{
|
|
134
|
+
avatarUrl: string;
|
|
135
|
+
}>;
|
|
136
|
+
/** Remove a user's uploaded avatar (clears it, or reverts to their latest OAuth picture if any).
|
|
137
|
+
* Session-authed, self-only. */
|
|
138
|
+
removeAvatar(userId: string): Promise<{
|
|
139
|
+
avatarUrl: string | null;
|
|
140
|
+
}>;
|
|
141
|
+
private request;
|
|
142
|
+
}
|
|
143
|
+
/** Convenience factory mirroring the SDK's other `create*` helpers. */
|
|
144
|
+
declare function createAccountsAdmin(opts: AccountsAdminOptions): AccountsAdmin;
|
|
145
|
+
|
|
146
|
+
export { AccountsAdmin, AccountsAdminError, type AccountsAdminOptions, type AccountsUser, type AppMember, type ChangePasswordResult, type GroupMember, type InviteResult, type ListUsersQuery, type ListUsersResult, createAccountsAdmin };
|
package/dist/admin.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// src/admin.ts
|
|
2
|
+
var AccountsAdminError = class extends Error {
|
|
3
|
+
constructor(status, message, body) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.status = status;
|
|
6
|
+
this.body = body;
|
|
7
|
+
this.name = "AccountsAdminError";
|
|
8
|
+
}
|
|
9
|
+
status;
|
|
10
|
+
body;
|
|
11
|
+
};
|
|
12
|
+
var AccountsAdmin = class {
|
|
13
|
+
constructor(opts) {
|
|
14
|
+
this.opts = opts;
|
|
15
|
+
const base = (opts.baseUrl ?? "").trim();
|
|
16
|
+
if (!base) throw new Error("AccountsAdmin: baseUrl is required.");
|
|
17
|
+
this.base = base.replace(/\/+$/, "");
|
|
18
|
+
this.f = opts.fetchImpl ?? fetch;
|
|
19
|
+
}
|
|
20
|
+
opts;
|
|
21
|
+
base;
|
|
22
|
+
f;
|
|
23
|
+
// ── Users ──────────────────────────────────────────────────────────────────
|
|
24
|
+
/** List/search users in a tenant. Session-authed (caller must be privileged in the tenant). */
|
|
25
|
+
async listUsers(query = {}) {
|
|
26
|
+
const qs = new URLSearchParams();
|
|
27
|
+
for (const [k, v] of Object.entries(query)) {
|
|
28
|
+
if (v !== void 0 && v !== null) qs.set(k, String(v));
|
|
29
|
+
}
|
|
30
|
+
const suffix = qs.toString() ? `?${qs.toString()}` : "";
|
|
31
|
+
const data = await this.request(
|
|
32
|
+
"GET",
|
|
33
|
+
`/api/auth/users${suffix}`,
|
|
34
|
+
{ auth: "session" }
|
|
35
|
+
);
|
|
36
|
+
return { users: data.users ?? [], ...data.pagination ? { pagination: data.pagination } : {} };
|
|
37
|
+
}
|
|
38
|
+
/** One user's full detail (includes `profileData`, `userGroups`, `apps`). Session-authed. */
|
|
39
|
+
async getUser(userId) {
|
|
40
|
+
return this.request("GET", `/api/auth/users/${encodeURIComponent(userId)}`, {
|
|
41
|
+
auth: "session"
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/** Update a user (self, or admin/owner/super-admin). `profileData` requires `appId`. Session-authed.
|
|
45
|
+
* Password is NOT set here — use {@link changePassword} (Accounts verifies the current password). */
|
|
46
|
+
async updateUser(userId, input) {
|
|
47
|
+
return this.request("PATCH", `/api/auth/users/${encodeURIComponent(userId)}`, {
|
|
48
|
+
auth: "session",
|
|
49
|
+
body: input
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/** Change the signed-in user's password. Session-authed; Accounts verifies `currentPassword` and,
|
|
53
|
+
* with `revokeOtherSessions`, logs the user out everywhere else. The target is the session's own
|
|
54
|
+
* user — no id is passed. */
|
|
55
|
+
async changePassword(input) {
|
|
56
|
+
return this.request("POST", "/api/auth/change-password", {
|
|
57
|
+
auth: "session",
|
|
58
|
+
body: {
|
|
59
|
+
currentPassword: input.currentPassword,
|
|
60
|
+
newPassword: input.newPassword,
|
|
61
|
+
confirmPassword: input.confirmPassword ?? input.newPassword,
|
|
62
|
+
...input.revokeOtherSessions !== void 0 ? { revokeOtherSessions: input.revokeOtherSessions } : {}
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
// ── App membership (invite / list / remove) ─────────────────────────────────
|
|
67
|
+
/** Invite an email to an app: adds an existing Accounts user immediately, else emails an accept
|
|
68
|
+
* link. Session-authed (caller must have access to the app). */
|
|
69
|
+
async inviteToApp(appSlug, input) {
|
|
70
|
+
return this.request("POST", `/api/auth/apps/${encodeURIComponent(appSlug)}/invites`, {
|
|
71
|
+
auth: "session",
|
|
72
|
+
body: input
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/** Everyone with access to an app. Session-authed. */
|
|
76
|
+
async listAppUsers(appSlug) {
|
|
77
|
+
const data = await this.request(
|
|
78
|
+
"GET",
|
|
79
|
+
`/api/auth/apps/${encodeURIComponent(appSlug)}/users`,
|
|
80
|
+
{ auth: "session" }
|
|
81
|
+
);
|
|
82
|
+
return data.users ?? [];
|
|
83
|
+
}
|
|
84
|
+
/** Revoke a user's access to an app. Session-authed. */
|
|
85
|
+
async removeAppUser(appSlug, userId) {
|
|
86
|
+
await this.request(
|
|
87
|
+
"DELETE",
|
|
88
|
+
`/api/auth/apps/${encodeURIComponent(appSlug)}/users?userId=${encodeURIComponent(userId)}`,
|
|
89
|
+
{ auth: "session" }
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
// ── Groups (agent picker) ───────────────────────────────────────────────────
|
|
93
|
+
/** Members of a user_group (the workspace's agents). Service-key authed (`stats:read`). */
|
|
94
|
+
async listGroupMembers(groupId, opts = {}) {
|
|
95
|
+
const suffix = opts.appId ? `?appId=${encodeURIComponent(opts.appId)}` : "";
|
|
96
|
+
const data = await this.request(
|
|
97
|
+
"GET",
|
|
98
|
+
`/api/auth/user-groups/${encodeURIComponent(groupId)}/members${suffix}`,
|
|
99
|
+
{ auth: "key" }
|
|
100
|
+
);
|
|
101
|
+
return data.members ?? [];
|
|
102
|
+
}
|
|
103
|
+
// ── App-scoped profile (availability / sound / any per-app preference) ───────
|
|
104
|
+
/** Read a user's app-scoped `profileData` (the store for per-app preferences). Session-authed. */
|
|
105
|
+
async getAppProfile(userId) {
|
|
106
|
+
const user = await this.getUser(userId);
|
|
107
|
+
return user.profileData ?? {};
|
|
108
|
+
}
|
|
109
|
+
/** Write app-scoped `profileData` fields for a user (e.g. availability, soundEnabled). All values
|
|
110
|
+
* are stored as strings by Accounts; booleans are serialised. Session-authed. */
|
|
111
|
+
async setAppProfile(userId, appId, profile) {
|
|
112
|
+
const profileData = {};
|
|
113
|
+
for (const [k, v] of Object.entries(profile)) profileData[k] = String(v);
|
|
114
|
+
return this.updateUser(userId, { appId, profileData });
|
|
115
|
+
}
|
|
116
|
+
// ── Avatar (uploaded image; social avatars are captured server-side on sign-in) ─
|
|
117
|
+
/** Upload a user's avatar image. The bytes are stored by Accounts and a public URL is returned +
|
|
118
|
+
* persisted (source = 'upload', which survives later OAuth sign-ins). Session-authed, self-only.
|
|
119
|
+
* `image` is the file bytes (Blob/Buffer/Uint8Array); `mimeType` + `filename` describe it. */
|
|
120
|
+
async uploadAvatar(userId, image, opts = {}) {
|
|
121
|
+
if (!this.opts.sessionToken) {
|
|
122
|
+
throw new Error("AccountsAdmin: a session token is required to upload an avatar.");
|
|
123
|
+
}
|
|
124
|
+
const mimeType = opts.mimeType ?? "application/octet-stream";
|
|
125
|
+
const blob = image instanceof Blob ? image : new Blob([image], { type: mimeType });
|
|
126
|
+
const form = new FormData();
|
|
127
|
+
form.append("avatar", blob, opts.filename ?? "avatar");
|
|
128
|
+
const res = await this.f(
|
|
129
|
+
new URL(`/api/auth/users/${encodeURIComponent(userId)}/avatar`, this.base),
|
|
130
|
+
{
|
|
131
|
+
method: "POST",
|
|
132
|
+
headers: { authorization: `Bearer ${this.opts.sessionToken}`, accept: "application/json" },
|
|
133
|
+
body: form
|
|
134
|
+
}
|
|
135
|
+
);
|
|
136
|
+
const text = await res.text();
|
|
137
|
+
const parsed = text ? JSON.parse(text) : {};
|
|
138
|
+
if (!res.ok) {
|
|
139
|
+
throw new AccountsAdminError(res.status, parsed?.error ?? "Avatar upload failed.", parsed);
|
|
140
|
+
}
|
|
141
|
+
return parsed;
|
|
142
|
+
}
|
|
143
|
+
/** Remove a user's uploaded avatar (clears it, or reverts to their latest OAuth picture if any).
|
|
144
|
+
* Session-authed, self-only. */
|
|
145
|
+
async removeAvatar(userId) {
|
|
146
|
+
return this.request(
|
|
147
|
+
"DELETE",
|
|
148
|
+
`/api/auth/users/${encodeURIComponent(userId)}/avatar`,
|
|
149
|
+
{ auth: "session" }
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
// ── internals ───────────────────────────────────────────────────────────────
|
|
153
|
+
async request(method, path, opts) {
|
|
154
|
+
const headers = { accept: "application/json" };
|
|
155
|
+
if (opts.auth === "session") {
|
|
156
|
+
if (!this.opts.sessionToken) {
|
|
157
|
+
throw new Error(`AccountsAdmin: a session token is required for ${method} ${path}.`);
|
|
158
|
+
}
|
|
159
|
+
headers.authorization = `Bearer ${this.opts.sessionToken}`;
|
|
160
|
+
} else {
|
|
161
|
+
if (!this.opts.serviceKey) {
|
|
162
|
+
throw new Error(`AccountsAdmin: a service key is required for ${method} ${path}.`);
|
|
163
|
+
}
|
|
164
|
+
headers["x-api-key"] = this.opts.serviceKey;
|
|
165
|
+
}
|
|
166
|
+
let bodyStr;
|
|
167
|
+
if (opts.body !== void 0) {
|
|
168
|
+
bodyStr = JSON.stringify(opts.body);
|
|
169
|
+
headers["content-type"] = "application/json";
|
|
170
|
+
}
|
|
171
|
+
const res = await this.f(new URL(path, this.base), { method, headers, body: bodyStr });
|
|
172
|
+
const text = await res.text();
|
|
173
|
+
let parsed;
|
|
174
|
+
try {
|
|
175
|
+
parsed = text ? JSON.parse(text) : void 0;
|
|
176
|
+
} catch {
|
|
177
|
+
parsed = text;
|
|
178
|
+
}
|
|
179
|
+
if (!res.ok) {
|
|
180
|
+
const message = (parsed && typeof parsed === "object" && "error" in parsed ? String(parsed.error) : void 0) ?? `Accounts request failed with status ${res.status}.`;
|
|
181
|
+
throw new AccountsAdminError(res.status, message, parsed);
|
|
182
|
+
}
|
|
183
|
+
return parsed;
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
function createAccountsAdmin(opts) {
|
|
187
|
+
return new AccountsAdmin(opts);
|
|
188
|
+
}
|
|
189
|
+
export {
|
|
190
|
+
AccountsAdmin,
|
|
191
|
+
AccountsAdminError,
|
|
192
|
+
createAccountsAdmin
|
|
193
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@~lyre/auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Shared Axis Accounts auth SDK — framework-agnostic session/identity core (HMAC-signed cookies, accounts login/callback/logout) plus an optional turnkey SvelteKit adapter.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -38,6 +38,10 @@
|
|
|
38
38
|
"types": "./dist/nest.d.ts",
|
|
39
39
|
"import": "./dist/nest.js"
|
|
40
40
|
},
|
|
41
|
+
"./admin": {
|
|
42
|
+
"types": "./dist/admin.d.ts",
|
|
43
|
+
"import": "./dist/admin.js"
|
|
44
|
+
},
|
|
41
45
|
"./package.json": "./package.json"
|
|
42
46
|
},
|
|
43
47
|
"files": [
|
|
@@ -46,7 +50,7 @@
|
|
|
46
50
|
"LICENSE"
|
|
47
51
|
],
|
|
48
52
|
"scripts": {
|
|
49
|
-
"build": "tsup src/index.ts src/sveltekit.ts src/nest.ts --format esm --dts --clean",
|
|
53
|
+
"build": "tsup src/index.ts src/sveltekit.ts src/nest.ts src/admin.ts --format esm --dts --clean",
|
|
50
54
|
"check": "tsc --noEmit"
|
|
51
55
|
},
|
|
52
56
|
"peerDependencies": {
|