@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@somewhatintelligent/guestlist",
3
- "version": "0.0.6-beta.1783733618.d38901c",
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.4-beta.1783733618.d38901c",
33
- "@somewhatintelligent/kit": "^0.0.3-beta.1783733618.d38901c",
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,
@@ -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-beta.1783733618.d38901c",
6
- commit: "d38901c2c285f8e92bed43496747988fbb24271f",
5
+ version: "0.0.6",
6
+ commit: "80b5bf630d4e7bf08ab0f9f7adf0463bb34fceff",
7
7
  } as const;