@somewhatintelligent/guestlist 0.0.6-beta.1783733618.d38901c → 0.0.6
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/package.json +3 -3
- package/src/entrypoint.ts +18 -0
- package/src/ops/orgs.ts +101 -1
- package/src/version.gen.ts +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@somewhatintelligent/guestlist",
|
|
3
|
-
"version": "0.0.6
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"publishConfig": {
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"@better-auth/core": "^1.6.9",
|
|
30
30
|
"@better-auth/oauth-provider": "^1.6.0",
|
|
31
31
|
"@better-auth/passkey": "^1.6.0",
|
|
32
|
-
"@somewhatintelligent/auth": "^0.0.
|
|
33
|
-
"@somewhatintelligent/kit": "^0.0.3
|
|
32
|
+
"@somewhatintelligent/auth": "^0.0.3",
|
|
33
|
+
"@somewhatintelligent/kit": "^0.0.3",
|
|
34
34
|
"better-auth": "^1.6.0",
|
|
35
35
|
"cookie-es": "^3.1.1",
|
|
36
36
|
"drizzle-orm": "^0.45.2",
|
package/src/entrypoint.ts
CHANGED
|
@@ -202,6 +202,12 @@ export function buildGuestlistEntrypoint<TEnv extends GuestlistEnv>(
|
|
|
202
202
|
return this.#gated(input.cookie, () => orgOps.getOrg(this.#inst(), input.id));
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
async adminUpdateOrg(input: WithCookie<{ id: string; name?: string; slug?: string }>) {
|
|
206
|
+
return this.#gated(input.cookie, () =>
|
|
207
|
+
orgOps.updateOrg(this.#inst(), input.id, { name: input.name, slug: input.slug }),
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
205
211
|
async adminSearchUsersByEmail(input: WithCookie<{ email: string }>) {
|
|
206
212
|
return this.#gated(input.cookie, () =>
|
|
207
213
|
orgOps.searchUsersByEmail(this.#inst(), input.email),
|
|
@@ -236,6 +242,18 @@ export function buildGuestlistEntrypoint<TEnv extends GuestlistEnv>(
|
|
|
236
242
|
);
|
|
237
243
|
}
|
|
238
244
|
|
|
245
|
+
async adminResendOrgInvitation(input: WithCookie<{ orgId: string; invitationId: string }>) {
|
|
246
|
+
return this.#gated(input.cookie, (operator) =>
|
|
247
|
+
orgOps.resendInvitation(
|
|
248
|
+
this.#inst(),
|
|
249
|
+
this.env.IDENTITY_URL,
|
|
250
|
+
operator,
|
|
251
|
+
input.orgId,
|
|
252
|
+
input.invitationId,
|
|
253
|
+
),
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
239
257
|
async adminCancelOrgInvitation(input: WithCookie<{ orgId: string; invitationId: string }>) {
|
|
240
258
|
return this.#gated(input.cookie, () =>
|
|
241
259
|
orgOps.cancelInvitation(this.#inst(), input.orgId, input.invitationId),
|
package/src/ops/orgs.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* `cancelInvitation`) fall back to direct Drizzle writes with the same
|
|
12
12
|
* invariants BA enforces (last-owner guards, pending-only cancellation).
|
|
13
13
|
*/
|
|
14
|
-
import { and, count, desc, eq, like } from "drizzle-orm";
|
|
14
|
+
import { and, count, desc, eq, like, ne } from "drizzle-orm";
|
|
15
15
|
import { ulid } from "@somewhatintelligent/kit/ids";
|
|
16
16
|
|
|
17
17
|
import type { GuestlistInstances } from "../instances";
|
|
@@ -163,6 +163,53 @@ export async function getOrg(inst: GuestlistInstances, id: string) {
|
|
|
163
163
|
return { ok: true as const, organization: org, members: memberRows, invitations: invitationRows };
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
export async function updateOrg(
|
|
167
|
+
inst: GuestlistInstances,
|
|
168
|
+
orgId: string,
|
|
169
|
+
input: { name?: string; slug?: string },
|
|
170
|
+
) {
|
|
171
|
+
const { db } = inst;
|
|
172
|
+
if (input.name !== undefined && (input.name.length < 2 || input.name.length > 60)) {
|
|
173
|
+
return err("validation", "name must be 2-60 chars");
|
|
174
|
+
}
|
|
175
|
+
if (
|
|
176
|
+
input.slug !== undefined &&
|
|
177
|
+
(input.slug.length < 2 || input.slug.length > 48 || !SLUG_RE.test(input.slug))
|
|
178
|
+
) {
|
|
179
|
+
return err("validation", "slug must be kebab-case, 2-48 chars");
|
|
180
|
+
}
|
|
181
|
+
const orgRows = await db
|
|
182
|
+
.select({ id: organization.id })
|
|
183
|
+
.from(organization)
|
|
184
|
+
.where(eq(organization.id, orgId))
|
|
185
|
+
.limit(1);
|
|
186
|
+
if (!orgRows[0]) return err("not_found");
|
|
187
|
+
// BA's `updateOrganization` has `requireHeaders: true` (session-bound) —
|
|
188
|
+
// same direct-Drizzle fallback as updateMemberRole. Slug uniqueness needs
|
|
189
|
+
// its own probe-select since BA's `checkSlug` isn't reachable on this
|
|
190
|
+
// no-session path.
|
|
191
|
+
if (input.slug !== undefined) {
|
|
192
|
+
const slugRows = await db
|
|
193
|
+
.select({ id: organization.id })
|
|
194
|
+
.from(organization)
|
|
195
|
+
.where(and(eq(organization.slug, input.slug), ne(organization.id, orgId)))
|
|
196
|
+
.limit(1);
|
|
197
|
+
if (slugRows[0]) return err("slug_taken");
|
|
198
|
+
}
|
|
199
|
+
const updates: { name?: string; slug?: string } = {};
|
|
200
|
+
if (input.name !== undefined) updates.name = input.name;
|
|
201
|
+
if (input.slug !== undefined) updates.slug = input.slug;
|
|
202
|
+
if (Object.keys(updates).length > 0) {
|
|
203
|
+
await db.update(organization).set(updates).where(eq(organization.id, orgId));
|
|
204
|
+
}
|
|
205
|
+
const updatedRows = await db
|
|
206
|
+
.select()
|
|
207
|
+
.from(organization)
|
|
208
|
+
.where(eq(organization.id, orgId))
|
|
209
|
+
.limit(1);
|
|
210
|
+
return { ok: true as const, organization: updatedRows[0] };
|
|
211
|
+
}
|
|
212
|
+
|
|
166
213
|
export async function addMember(
|
|
167
214
|
inst: GuestlistInstances,
|
|
168
215
|
orgId: string,
|
|
@@ -305,6 +352,59 @@ export async function createInvitation(
|
|
|
305
352
|
};
|
|
306
353
|
}
|
|
307
354
|
|
|
355
|
+
export async function resendInvitation(
|
|
356
|
+
inst: GuestlistInstances,
|
|
357
|
+
identityUrl: string,
|
|
358
|
+
operator: Authed,
|
|
359
|
+
orgId: string,
|
|
360
|
+
invitationId: string,
|
|
361
|
+
) {
|
|
362
|
+
const { db } = inst;
|
|
363
|
+
const rows = await db
|
|
364
|
+
.select({
|
|
365
|
+
id: invitation.id,
|
|
366
|
+
status: invitation.status,
|
|
367
|
+
email: invitation.email,
|
|
368
|
+
role: invitation.role,
|
|
369
|
+
})
|
|
370
|
+
.from(invitation)
|
|
371
|
+
.where(and(eq(invitation.id, invitationId), eq(invitation.organizationId, orgId)))
|
|
372
|
+
.limit(1);
|
|
373
|
+
const inv = rows[0];
|
|
374
|
+
if (!inv) return err("invitation_not_found");
|
|
375
|
+
if (inv.status !== "pending") return err("invitation_not_pending", inv.status);
|
|
376
|
+
// Same 7-day window as createInvitation. The id is reused, so previously
|
|
377
|
+
// copied accept links stay valid.
|
|
378
|
+
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
|
|
379
|
+
await db.update(invitation).set({ expiresAt }).where(eq(invitation.id, invitationId));
|
|
380
|
+
const orgRows = await db
|
|
381
|
+
.select({ name: organization.name })
|
|
382
|
+
.from(organization)
|
|
383
|
+
.where(eq(organization.id, orgId))
|
|
384
|
+
.limit(1);
|
|
385
|
+
// Unlike createInvitation (raw-link-in-UI, no send), resend's whole point
|
|
386
|
+
// is delivery — emit the org-invitation email event with the operator as
|
|
387
|
+
// inviter. Best-effort: the extended expiry and accept link stand even if
|
|
388
|
+
// the consumer's transport fails.
|
|
389
|
+
let emailSent = false;
|
|
390
|
+
try {
|
|
391
|
+
await inst.sendEmail({
|
|
392
|
+
kind: "org-invitation",
|
|
393
|
+
to: { email: inv.email },
|
|
394
|
+
url: `${identityUrl}/orgs/accept/${inv.id}`,
|
|
395
|
+
organizationName: orgRows[0]?.name ?? "",
|
|
396
|
+
// `invitation.role` is nullable in the BA schema; every write path
|
|
397
|
+
// sets it, but default to BA's member role if absent.
|
|
398
|
+
role: inv.role ?? "member",
|
|
399
|
+
inviter: { name: operator.user.name, email: operator.user.email },
|
|
400
|
+
});
|
|
401
|
+
emailSent = true;
|
|
402
|
+
} catch {
|
|
403
|
+
// Consumer transport failure — surface via emailSent, don't fail the op.
|
|
404
|
+
}
|
|
405
|
+
return { ok: true as const, emailSent, expiresAt: expiresAt.getTime() };
|
|
406
|
+
}
|
|
407
|
+
|
|
308
408
|
export async function cancelInvitation(
|
|
309
409
|
inst: GuestlistInstances,
|
|
310
410
|
orgId: string,
|
package/src/version.gen.ts
CHANGED
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
// values are dev placeholders, identical to the runtime fallbacks.
|
|
3
3
|
export const PKG = {
|
|
4
4
|
name: "@somewhatintelligent/guestlist",
|
|
5
|
-
version: "0.0.6
|
|
6
|
-
commit: "
|
|
5
|
+
version: "0.0.6",
|
|
6
|
+
commit: "80b5bf630d4e7bf08ab0f9f7adf0463bb34fceff",
|
|
7
7
|
} as const;
|