@~lyre/auth 0.3.0 → 0.5.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/dist/admin.d.ts CHANGED
@@ -61,11 +61,54 @@ interface AppMember {
61
61
  via?: 'member' | 'role';
62
62
  }
63
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';
64
+ /** `invited` = an invite was created and an accept link emailed;
65
+ * `already_member` = that email already has access to the app (returned with HTTP 409).
66
+ * (`added` is legacy: Accounts no longer grants access without acceptance.) */
67
+ status: 'invited' | 'already_member' | 'added';
68
+ /** The workspace the invitee joins on acceptance, echoed back; null when none was requested. */
69
+ userGroupId?: string | null;
67
70
  [key: string]: unknown;
68
71
  }
72
+ /**
73
+ * A profile field an app collects about a user — the spec a consumer renders a form input from.
74
+ * Mirrors Accounts' `fields` registry, so a consumer never hardcodes one app's idea of a person.
75
+ */
76
+ interface ProfileFieldSpec {
77
+ id: string;
78
+ fieldName: string;
79
+ label: string;
80
+ fieldType: string;
81
+ required: boolean;
82
+ hidden: boolean;
83
+ enumValues: string[] | null;
84
+ inputPrefix: string | null;
85
+ validationRegex: string | null;
86
+ validationMessage: string | null;
87
+ displayOrder: number;
88
+ }
89
+ /** Whether an email can be invited to an app. See `checkInviteEmail`. */
90
+ interface InviteEmailCheck {
91
+ email: string;
92
+ /** `member` = already has app access; `invited` = a live invite exists; `available` = go ahead. */
93
+ state: 'member' | 'invited' | 'available';
94
+ /** Whether an Accounts user already exists for this address (they keep their own profile). */
95
+ hasAccount: boolean;
96
+ /** When the pending invite lapses — only for `invited`. */
97
+ expiresAt?: string;
98
+ }
99
+ /** A pending (not yet accepted, not yet expired) app invitation. */
100
+ interface PendingInvite {
101
+ id: string;
102
+ email: string;
103
+ /** The workspace the invitee joins on acceptance; null for a group-less invite. */
104
+ userGroupId: string | null;
105
+ /** Values captured on the invite form, keyed by field name. Prefills the accept form. */
106
+ profileData?: Record<string, string>;
107
+ createdAt: string;
108
+ expiresAt: string;
109
+ /** The inviter's email, when still resolvable. */
110
+ invitedByEmail?: string | null;
111
+ }
69
112
  interface GroupMember {
70
113
  id: string;
71
114
  email: string;
@@ -106,11 +149,36 @@ declare class AccountsAdmin {
106
149
  confirmPassword?: string;
107
150
  revokeOtherSessions?: boolean;
108
151
  }): 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). */
152
+ /**
153
+ * Invite an email to an app, emailing them an accept link. Session-authed (the caller must have
154
+ * access to the app).
155
+ *
156
+ * Pass `userGroupId` to say which WORKSPACE the invitee joins when they accept. Omit it and they
157
+ * join the app but no workspace — Accounts then treats them as a brand-new user on first login and
158
+ * sends them to create their own, instead of landing in the workspace that invited them. Accounts
159
+ * validates the group against the CALLER's own memberships, so this cannot target a foreign group.
160
+ */
111
161
  inviteToApp(appSlug: string, input: {
112
162
  email: string;
163
+ userGroupId?: string | null;
164
+ /** Profile values from the invite form, keyed by field name (see `listAppProfileFields`).
165
+ * Accounts drops any key the app does not collect, so authorization-shaped values
166
+ * (role, assignability) can never be set from here. */
167
+ profileData?: Record<string, string> | null;
113
168
  }): Promise<InviteResult>;
169
+ /**
170
+ * The profile fields this app collects about a user — the spec to render an invite form from.
171
+ * Session-authed. Never includes email: that is the invite's identity, not a profile value.
172
+ */
173
+ listAppProfileFields(appSlug: string): Promise<ProfileFieldSpec[]>;
174
+ /**
175
+ * Whether an email can be invited — for a live check on an invite form, so the inviter learns
176
+ * BEFORE submitting that someone is already a member or already invited. Email only, exact match.
177
+ * Session-authed.
178
+ */
179
+ checkInviteEmail(appSlug: string, email: string): Promise<InviteEmailCheck>;
180
+ /** Revoke a pending invite (withdraw one sent in error). Session-authed. */
181
+ revokeAppInvite(appSlug: string, inviteId: string): Promise<void>;
114
182
  /** Everyone with access to an app. Session-authed. */
115
183
  listAppUsers(appSlug: string): Promise<AppMember[]>;
116
184
  /** Revoke a user's access to an app. Session-authed. */
@@ -118,7 +186,43 @@ declare class AccountsAdmin {
118
186
  /** Members of a user_group (the workspace's agents). Service-key authed (`stats:read`). */
119
187
  listGroupMembers(groupId: string, opts?: {
120
188
  appId?: string;
189
+ search?: string;
190
+ limit?: number;
191
+ offset?: number;
121
192
  }): Promise<GroupMember[]>;
193
+ /**
194
+ * A PAGE of a group's members, plus the total before paging — what a paginated, searchable user
195
+ * list needs. `listGroupMembers` stays the "give me everyone" convenience for the agent picker.
196
+ *
197
+ * Search and paging are applied by Accounts in SQL: fetching every member to filter in the browser
198
+ * does not scale, and leaves the pager unable to know how many pages exist. Omit `limit` and the
199
+ * full list comes back (with `totalCount` equal to its length).
200
+ */
201
+ listGroupMembersPage(groupId: string, opts?: {
202
+ appId?: string;
203
+ search?: string;
204
+ limit?: number;
205
+ offset?: number;
206
+ }): Promise<{
207
+ members: GroupMember[];
208
+ totalCount: number;
209
+ }>;
210
+ /**
211
+ * The app's PENDING invites — people invited but not yet accepted. Session-authed.
212
+ *
213
+ * An invitee is not a membership, so they appear in no member list; without this a successful
214
+ * invite looks like nothing happened until the person accepts. Pass `userGroupId` to scope to one
215
+ * workspace. Accounts excludes consumed and expired invites.
216
+ */
217
+ listAppInvites(appSlug: string, opts?: {
218
+ userGroupId?: string;
219
+ search?: string;
220
+ limit?: number;
221
+ offset?: number;
222
+ }): Promise<{
223
+ invites: PendingInvite[];
224
+ totalCount: number;
225
+ }>;
122
226
  /** Read a user's app-scoped `profileData` (the store for per-app preferences). Session-authed. */
123
227
  getAppProfile(userId: string): Promise<Record<string, string>>;
124
228
  /** Write app-scoped `profileData` fields for a user (e.g. availability, soundEnabled). All values
@@ -143,4 +247,4 @@ declare class AccountsAdmin {
143
247
  /** Convenience factory mirroring the SDK's other `create*` helpers. */
144
248
  declare function createAccountsAdmin(opts: AccountsAdminOptions): AccountsAdmin;
145
249
 
146
- export { AccountsAdmin, AccountsAdminError, type AccountsAdminOptions, type AccountsUser, type AppMember, type ChangePasswordResult, type GroupMember, type InviteResult, type ListUsersQuery, type ListUsersResult, createAccountsAdmin };
250
+ export { AccountsAdmin, AccountsAdminError, type AccountsAdminOptions, type AccountsUser, type AppMember, type ChangePasswordResult, type GroupMember, type InviteEmailCheck, type InviteResult, type ListUsersQuery, type ListUsersResult, type PendingInvite, type ProfileFieldSpec, createAccountsAdmin };
package/dist/admin.js CHANGED
@@ -64,14 +64,53 @@ var AccountsAdmin = class {
64
64
  });
65
65
  }
66
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). */
67
+ /**
68
+ * Invite an email to an app, emailing them an accept link. Session-authed (the caller must have
69
+ * access to the app).
70
+ *
71
+ * Pass `userGroupId` to say which WORKSPACE the invitee joins when they accept. Omit it and they
72
+ * join the app but no workspace — Accounts then treats them as a brand-new user on first login and
73
+ * sends them to create their own, instead of landing in the workspace that invited them. Accounts
74
+ * validates the group against the CALLER's own memberships, so this cannot target a foreign group.
75
+ */
69
76
  async inviteToApp(appSlug, input) {
70
77
  return this.request("POST", `/api/auth/apps/${encodeURIComponent(appSlug)}/invites`, {
71
78
  auth: "session",
72
79
  body: input
73
80
  });
74
81
  }
82
+ /**
83
+ * The profile fields this app collects about a user — the spec to render an invite form from.
84
+ * Session-authed. Never includes email: that is the invite's identity, not a profile value.
85
+ */
86
+ async listAppProfileFields(appSlug) {
87
+ const data = await this.request(
88
+ "GET",
89
+ `/api/auth/apps/${encodeURIComponent(appSlug)}/profile-fields`,
90
+ { auth: "session" }
91
+ );
92
+ return data.fields ?? [];
93
+ }
94
+ /**
95
+ * Whether an email can be invited — for a live check on an invite form, so the inviter learns
96
+ * BEFORE submitting that someone is already a member or already invited. Email only, exact match.
97
+ * Session-authed.
98
+ */
99
+ async checkInviteEmail(appSlug, email) {
100
+ return this.request(
101
+ "GET",
102
+ `/api/auth/apps/${encodeURIComponent(appSlug)}/invites/lookup?email=${encodeURIComponent(email)}`,
103
+ { auth: "session" }
104
+ );
105
+ }
106
+ /** Revoke a pending invite (withdraw one sent in error). Session-authed. */
107
+ async revokeAppInvite(appSlug, inviteId) {
108
+ await this.request(
109
+ "DELETE",
110
+ `/api/auth/apps/${encodeURIComponent(appSlug)}/invites?inviteId=${encodeURIComponent(inviteId)}`,
111
+ { auth: "session" }
112
+ );
113
+ }
75
114
  /** Everyone with access to an app. Session-authed. */
76
115
  async listAppUsers(appSlug) {
77
116
  const data = await this.request(
@@ -92,13 +131,53 @@ var AccountsAdmin = class {
92
131
  // ── Groups (agent picker) ───────────────────────────────────────────────────
93
132
  /** Members of a user_group (the workspace's agents). Service-key authed (`stats:read`). */
94
133
  async listGroupMembers(groupId, opts = {}) {
95
- const suffix = opts.appId ? `?appId=${encodeURIComponent(opts.appId)}` : "";
134
+ const { members } = await this.listGroupMembersPage(groupId, opts);
135
+ return members;
136
+ }
137
+ /**
138
+ * A PAGE of a group's members, plus the total before paging — what a paginated, searchable user
139
+ * list needs. `listGroupMembers` stays the "give me everyone" convenience for the agent picker.
140
+ *
141
+ * Search and paging are applied by Accounts in SQL: fetching every member to filter in the browser
142
+ * does not scale, and leaves the pager unable to know how many pages exist. Omit `limit` and the
143
+ * full list comes back (with `totalCount` equal to its length).
144
+ */
145
+ async listGroupMembersPage(groupId, opts = {}) {
146
+ const qs = new URLSearchParams();
147
+ if (opts.appId) qs.set("appId", opts.appId);
148
+ if (opts.search) qs.set("search", opts.search);
149
+ if (opts.limit !== void 0) qs.set("limit", String(opts.limit));
150
+ if (opts.offset !== void 0) qs.set("offset", String(opts.offset));
151
+ const suffix = qs.size ? `?${qs}` : "";
96
152
  const data = await this.request(
97
153
  "GET",
98
154
  `/api/auth/user-groups/${encodeURIComponent(groupId)}/members${suffix}`,
99
155
  { auth: "key" }
100
156
  );
101
- return data.members ?? [];
157
+ const members = data.members ?? [];
158
+ return { members, totalCount: data.totalCount ?? members.length };
159
+ }
160
+ /**
161
+ * The app's PENDING invites — people invited but not yet accepted. Session-authed.
162
+ *
163
+ * An invitee is not a membership, so they appear in no member list; without this a successful
164
+ * invite looks like nothing happened until the person accepts. Pass `userGroupId` to scope to one
165
+ * workspace. Accounts excludes consumed and expired invites.
166
+ */
167
+ async listAppInvites(appSlug, opts = {}) {
168
+ const qs = new URLSearchParams();
169
+ if (opts.userGroupId) qs.set("userGroupId", opts.userGroupId);
170
+ if (opts.search) qs.set("search", opts.search);
171
+ if (opts.limit !== void 0) qs.set("limit", String(opts.limit));
172
+ if (opts.offset !== void 0) qs.set("offset", String(opts.offset));
173
+ const suffix = qs.size ? `?${qs}` : "";
174
+ const data = await this.request(
175
+ "GET",
176
+ `/api/auth/apps/${encodeURIComponent(appSlug)}/invites${suffix}`,
177
+ { auth: "session" }
178
+ );
179
+ const invites = data.invites ?? [];
180
+ return { invites, totalCount: data.totalCount ?? invites.length };
102
181
  }
103
182
  // ── App-scoped profile (availability / sound / any per-app preference) ───────
104
183
  /** Read a user's app-scoped `profileData` (the store for per-app preferences). Session-authed. */
package/package.json CHANGED
@@ -1,73 +1,73 @@
1
1
  {
2
- "name": "@~lyre/auth",
3
- "version": "0.3.0",
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
- "type": "module",
6
- "sideEffects": false,
7
- "license": "MIT",
8
- "author": "Britam Trust Services / Lyre",
9
- "repository": {
10
- "type": "git",
11
- "url": "git+https://github.com/kigathi-chege/lyre-auth-sdk.git"
12
- },
13
- "homepage": "https://github.com/kigathi-chege/lyre-auth-sdk#readme",
14
- "bugs": {
15
- "url": "https://github.com/kigathi-chege/lyre-auth-sdk/issues"
16
- },
17
- "keywords": [
18
- "auth",
19
- "sso",
20
- "accounts",
21
- "session",
22
- "sveltekit",
23
- "hmac",
24
- "lyre"
25
- ],
26
- "main": "./dist/index.js",
27
- "types": "./dist/index.d.ts",
28
- "exports": {
29
- ".": {
30
- "types": "./dist/index.d.ts",
31
- "import": "./dist/index.js"
32
- },
33
- "./sveltekit": {
34
- "types": "./dist/sveltekit.d.ts",
35
- "import": "./dist/sveltekit.js"
36
- },
37
- "./nest": {
38
- "types": "./dist/nest.d.ts",
39
- "import": "./dist/nest.js"
40
- },
41
- "./admin": {
42
- "types": "./dist/admin.d.ts",
43
- "import": "./dist/admin.js"
44
- },
45
- "./package.json": "./package.json"
46
- },
47
- "files": [
48
- "dist",
49
- "README.md",
50
- "LICENSE"
51
- ],
52
- "scripts": {
53
- "build": "tsup src/index.ts src/sveltekit.ts src/nest.ts src/admin.ts --format esm --dts --clean",
54
- "check": "tsc --noEmit"
55
- },
56
- "peerDependencies": {
57
- "@sveltejs/kit": "^2.0.0"
58
- },
59
- "peerDependenciesMeta": {
60
- "@sveltejs/kit": {
61
- "optional": true
62
- }
63
- },
64
- "devDependencies": {
65
- "@sveltejs/kit": "^2.0.0",
66
- "@types/node": "^22",
67
- "tsup": "^8.3.5",
68
- "typescript": "^6.0.2"
69
- },
70
- "publishConfig": {
71
- "access": "public"
72
- }
2
+ "name": "@~lyre/auth",
3
+ "version": "0.5.0",
4
+ "description": "Shared Axis Accounts auth SDK \u2014 framework-agnostic session/identity core (HMAC-signed cookies, accounts login/callback/logout) plus an optional turnkey SvelteKit adapter.",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "license": "MIT",
8
+ "author": "Britam Trust Services / Lyre",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/kigathi-chege/lyre-auth-sdk.git"
12
+ },
13
+ "homepage": "https://github.com/kigathi-chege/lyre-auth-sdk#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/kigathi-chege/lyre-auth-sdk/issues"
16
+ },
17
+ "keywords": [
18
+ "auth",
19
+ "sso",
20
+ "accounts",
21
+ "session",
22
+ "sveltekit",
23
+ "hmac",
24
+ "lyre"
25
+ ],
26
+ "main": "./dist/index.js",
27
+ "types": "./dist/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.js"
32
+ },
33
+ "./sveltekit": {
34
+ "types": "./dist/sveltekit.d.ts",
35
+ "import": "./dist/sveltekit.js"
36
+ },
37
+ "./nest": {
38
+ "types": "./dist/nest.d.ts",
39
+ "import": "./dist/nest.js"
40
+ },
41
+ "./admin": {
42
+ "types": "./dist/admin.d.ts",
43
+ "import": "./dist/admin.js"
44
+ },
45
+ "./package.json": "./package.json"
46
+ },
47
+ "files": [
48
+ "dist",
49
+ "README.md",
50
+ "LICENSE"
51
+ ],
52
+ "scripts": {
53
+ "build": "tsup src/index.ts src/sveltekit.ts src/nest.ts src/admin.ts --format esm --dts --clean",
54
+ "check": "tsc --noEmit"
55
+ },
56
+ "peerDependencies": {
57
+ "@sveltejs/kit": "^2.0.0"
58
+ },
59
+ "peerDependenciesMeta": {
60
+ "@sveltejs/kit": {
61
+ "optional": true
62
+ }
63
+ },
64
+ "devDependencies": {
65
+ "@sveltejs/kit": "^2.0.0",
66
+ "@types/node": "^22",
67
+ "tsup": "^8.3.5",
68
+ "typescript": "^6.0.2"
69
+ },
70
+ "publishConfig": {
71
+ "access": "public"
72
+ }
73
73
  }