From 7a57be05b4f8ba4217aa545b5e7fa802c2eb092e Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 26 Apr 2026 12:30:34 -0400 Subject: [PATCH 1/8] feat(membership): track provisioning source on each org membership Adds organization_memberships.provisioning_source so we know how each membership came to exist: verified_domain, invited, admin_added, webhook, or null (pre-migration rows). The originating endpoint stages source + seat_type into invitation_seat_types (new source column) and the organization_membership.created webhook reads both back via consumeInvitationSeatType, applying provisioning_source to the local cache row. The upsert preserves an existing source on conflict so a later 'webhook' upsert can't wipe a more-specific origin. autoLinkByVerifiedDomain now stages source='verified_domain' before calling WorkOS create. Path 1 of /members/by-email (and the existing /invitations and resend-invitation flows) stage 'invited'. Path 2 stages 'admin_added'. Sets up the new-member digest in the auto-provision notification feature so org owners can see which auto-joined members arrived via verified-domain vs. were explicitly invited. Co-Authored-By: Claude Opus 4.7 (1M context) --- .changeset/provisioning-source-attribution.md | 20 ++++ server/src/db/membership-db.ts | 69 +++++++++-- ...ization_membership_provisioning_source.sql | 30 +++++ server/src/routes/organizations.ts | 31 ++--- server/src/routes/workos-webhooks.ts | 13 ++- .../integration/membership-webhook.test.ts | 109 ++++++++++++++++-- 6 files changed, 238 insertions(+), 34 deletions(-) create mode 100644 .changeset/provisioning-source-attribution.md create mode 100644 server/src/db/migrations/436_organization_membership_provisioning_source.sql diff --git a/.changeset/provisioning-source-attribution.md b/.changeset/provisioning-source-attribution.md new file mode 100644 index 0000000000..785d163761 --- /dev/null +++ b/.changeset/provisioning-source-attribution.md @@ -0,0 +1,20 @@ +--- +--- + +feat(membership): track provisioning source on each org membership + +Each row in `organization_memberships` now carries a `provisioning_source` tag identifying how it came to exist: + +- `verified_domain` — `autoLinkByVerifiedDomain` matched the user's email domain to a verified org with auto-provision on +- `invited` — accepted via `POST /:orgId/invitations` or `/members/by-email` Path 1 +- `admin_added` — created via `/members/by-email` Path 2 (admin/owner direct add) +- `webhook` — surfaced by an `organization_membership.created` event with no staged source (e.g. someone added the membership directly in the WorkOS dashboard) +- `null` — pre-existing rows that haven't been touched since this migration + +The originating endpoint stages source + seat_type into `invitation_seat_types` (a new `source` column there). The `organization_membership.created` webhook handler reads both back via `consumeInvitationSeatType` and writes `provisioning_source` on the local cache row. The upsert preserves an existing source on conflict, so a subsequent webhook upsert can't wipe a more-specific origin. + +Sets up the new-member digest in the auto-provision notification feature so org owners can see which auto-joined members showed up via verified-domain vs. were explicitly invited. + +## Migration + +`436_organization_membership_provisioning_source.sql` adds the columns and an index keyed on `(workos_organization_id, provisioning_source, created_at DESC)` for the digest query. diff --git a/server/src/db/membership-db.ts b/server/src/db/membership-db.ts index c6fbfb73f6..a1601bea52 100644 --- a/server/src/db/membership-db.ts +++ b/server/src/db/membership-db.ts @@ -13,6 +13,14 @@ const logger = createLogger('membership-db'); // ── Types ──────────────────────────────────────────────────────────── +/** Tracks how each organization_memberships row came to exist. */ +export type ProvisioningSource = + | 'verified_domain' // autoLinkByVerifiedDomain + | 'invited' // POST /:orgId/invitations or /members/by-email Path 1 + | 'admin_added' // /members/by-email Path 2 direct add + | 'webhook' // organization_membership.created with no staged source + | 'unknown'; + export interface MembershipUpsertParams { user_id: string; organization_id: string; @@ -23,6 +31,13 @@ export interface MembershipUpsertParams { role: string; // raw role slug from WorkOS (e.g. 'member', 'admin', 'owner') seat_type: string; // resolved seat type ('contributor' | 'community_only') has_explicit_seat_type: boolean; + /** + * Provisioning source to record on the local cache row. Only written when + * the row is being inserted (or when the existing row has NULL/'unknown' + * source) — once a membership is tagged, subsequent webhook upserts don't + * overwrite the original attribution. + */ + provisioning_source?: ProvisioningSource; } export interface MembershipUpsertResult { @@ -57,6 +72,7 @@ export async function upsertOrganizationMembership( last_name, role, seat_type, + provisioning_source, synced_at ) VALUES ( $1, $2, $3, $4, $5, $6, @@ -70,7 +86,7 @@ export async function upsertOrganizationMembership( WHEN $7 = '__auto__' THEN 'member' ELSE $7 END, - $8, NOW() + $8, $10, NOW() ) ON CONFLICT (workos_user_id, workos_organization_id) DO UPDATE SET @@ -83,6 +99,9 @@ export async function upsertOrganizationMembership( WHEN $9::boolean THEN EXCLUDED.seat_type ELSE organization_memberships.seat_type END, + -- Don't overwrite an existing attribution; later webhooks would be the + -- 'webhook' source and would otherwise wipe a more specific origin. + provisioning_source = COALESCE(organization_memberships.provisioning_source, EXCLUDED.provisioning_source), synced_at = NOW(), updated_at = NOW() RETURNING role`, @@ -96,6 +115,7 @@ export async function upsertOrganizationMembership( effectiveRole, params.seat_type, params.has_explicit_seat_type, + params.provisioning_source ?? null, ], ); @@ -136,23 +156,28 @@ export async function deleteOrganizationMembership( // ── Invitation seat type ───────────────────────────────────────────── /** - * Consume any pending seat_type assignment from an invitation. - * Returns the seat type if one was found, or null. + * Consume any pending seat_type and provisioning_source staged by the endpoint + * that triggered the membership creation. Returns both fields when a row is + * found; null when no staging row exists. */ export async function consumeInvitationSeatType( organizationId: string, email: string, -): Promise { +): Promise<{ seat_type: string; source: ProvisioningSource | null } | null> { const pool = getPool(); - const result = await pool.query<{ seat_type: string }>( + const result = await pool.query<{ seat_type: string; source: string | null }>( `DELETE FROM invitation_seat_types WHERE workos_organization_id = $1 AND lower(email) = lower($2) - RETURNING seat_type`, + RETURNING seat_type, source`, [organizationId, email], ); - return result.rows[0]?.seat_type ?? null; + if (!result.rows[0]) return null; + return { + seat_type: result.rows[0].seat_type, + source: (result.rows[0].source as ProvisioningSource | null) ?? null, + }; } // ── Successor promotion query ──────────────────────────────────────── @@ -260,6 +285,23 @@ export async function autoLinkByVerifiedDomain( if (userAlreadyMember) return null; + // Stage the provisioning source so the organization_membership.created + // webhook handler can record 'verified_domain' on the local cache row. + // Clear any stale (org, email) staging row first; consumeInvitationSeatType + // matches by (org, email) and we don't want a leftover row from a prior + // failed attempt to win. + const stagingKey = `verified_domain_${orgId}_${userId}`; + await pool.query( + 'DELETE FROM invitation_seat_types WHERE workos_organization_id = $1 AND lower(email) = lower($2)', + [orgId, email], + ); + await pool.query( + `INSERT INTO invitation_seat_types (workos_invitation_id, workos_organization_id, email, seat_type, source) + VALUES ($1, $2, $3, 'community_only', 'verified_domain') + ON CONFLICT (workos_invitation_id) DO UPDATE SET seat_type = EXCLUDED.seat_type, source = EXCLUDED.source`, + [stagingKey, orgId, email], + ); + // Always create as member. Auto-promotion to owner for ownerless orgs is // handled atomically by upsertOrganizationMembership when the // organization_membership.created webhook fires — that path uses a NOT EXISTS @@ -280,6 +322,19 @@ export async function autoLinkByVerifiedDomain( logger.info({ userId, orgId }, 'Auto-link skipped: membership already exists in WorkOS'); return { organizationId: orgId, organizationName: orgName, role: 'member' }; } + // Roll back the staging row so a stale 'verified_domain' source can't be + // consumed by an unrelated future invite for the same (org, email) pair. + try { + await pool.query( + 'DELETE FROM invitation_seat_types WHERE workos_invitation_id = $1', + [stagingKey], + ); + } catch (rollbackErr) { + logger.error( + { err: rollbackErr, userId, orgId, email, stagingKey }, + 'CRITICAL: failed to rollback verified_domain staging row after createOrganizationMembership failure — manually delete row to avoid source leak', + ); + } logger.warn({ err, userId, orgId }, 'Failed to auto-link user to organization'); return null; } diff --git a/server/src/db/migrations/436_organization_membership_provisioning_source.sql b/server/src/db/migrations/436_organization_membership_provisioning_source.sql new file mode 100644 index 0000000000..98f4cacba3 --- /dev/null +++ b/server/src/db/migrations/436_organization_membership_provisioning_source.sql @@ -0,0 +1,30 @@ +-- Track how each organization membership came to exist. +-- +-- Existing rows are NULL ('unknown'). New rows are tagged at creation by the +-- code path that triggered them: autoLinkByVerifiedDomain → 'verified_domain', +-- /members/by-email Path 1 → 'invited', Path 2 → 'admin_added', +-- POST /:orgId/invitations → 'invited', everything else (sync from WorkOS, +-- webhook for an externally-created membership) → 'webhook'. +-- +-- Used by the new-member digest in the auto-provision notification feature so +-- org owners can see which auto-joined members showed up via verified-domain +-- vs. were explicitly invited. + +ALTER TABLE organization_memberships + ADD COLUMN IF NOT EXISTS provisioning_source VARCHAR(32); + +COMMENT ON COLUMN organization_memberships.provisioning_source IS + 'How this membership was created: verified_domain, invited, admin_added, webhook, unknown'; + +CREATE INDEX IF NOT EXISTS idx_organization_memberships_provisioning_source + ON organization_memberships(workos_organization_id, provisioning_source, created_at DESC) + WHERE provisioning_source IS NOT NULL; + +-- Mirror on the invitation_seat_types staging table so the webhook handler can +-- read the source set by the originating endpoint and apply it to the local +-- cache row when the membership.created event arrives. +ALTER TABLE invitation_seat_types + ADD COLUMN IF NOT EXISTS source VARCHAR(32); + +COMMENT ON COLUMN invitation_seat_types.source IS + 'Provisioning source to apply to the membership when this staging row is consumed'; diff --git a/server/src/routes/organizations.ts b/server/src/routes/organizations.ts index b6acd21f2c..6009d9b719 100644 --- a/server/src/routes/organizations.ts +++ b/server/src/routes/organizations.ts @@ -2400,11 +2400,11 @@ export function createOrganizationsRouter(): Router { roleSlug: role || 'member', }); - // Persist seat_type intent for when the invitation is accepted + // Persist seat_type intent + provisioning source for when the invitation is accepted await query( - `INSERT INTO invitation_seat_types (workos_invitation_id, workos_organization_id, email, seat_type) - VALUES ($1, $2, $3, $4) - ON CONFLICT (workos_invitation_id) DO UPDATE SET seat_type = EXCLUDED.seat_type`, + `INSERT INTO invitation_seat_types (workos_invitation_id, workos_organization_id, email, seat_type, source) + VALUES ($1, $2, $3, $4, 'invited') + ON CONFLICT (workos_invitation_id) DO UPDATE SET seat_type = EXCLUDED.seat_type, source = EXCLUDED.source`, [invitation.id, orgId, email, seatType] ); @@ -2575,10 +2575,10 @@ export function createOrganizationsRouter(): Router { roleSlug: 'member', }); - // Persist seat_type intent for the new invitation + // Persist seat_type intent + provisioning source for the new invitation await query( - `INSERT INTO invitation_seat_types (workos_invitation_id, workos_organization_id, email, seat_type) - VALUES ($1, $2, $3, $4)`, + `INSERT INTO invitation_seat_types (workos_invitation_id, workos_organization_id, email, seat_type, source) + VALUES ($1, $2, $3, $4, 'invited')`, [newInvitation.id, orgId, invitation.email, preservedSeatType] ); @@ -2751,12 +2751,13 @@ export function createOrganizationsRouter(): Router { roleSlug: 'member', }); - // Persist seat_type intent so the webhook handler picks it up when the - // invitee accepts (mirrors the existing /invitations endpoint). + // Persist seat_type intent + provisioning source so the webhook + // handler picks them up when the invitee accepts (mirrors the + // existing /invitations endpoint). await query( - `INSERT INTO invitation_seat_types (workos_invitation_id, workos_organization_id, email, seat_type) - VALUES ($1, $2, $3, $4) - ON CONFLICT (workos_invitation_id) DO UPDATE SET seat_type = EXCLUDED.seat_type`, + `INSERT INTO invitation_seat_types (workos_invitation_id, workos_organization_id, email, seat_type, source) + VALUES ($1, $2, $3, $4, 'invited') + ON CONFLICT (workos_invitation_id) DO UPDATE SET seat_type = EXCLUDED.seat_type, source = EXCLUDED.source`, [invitation.id, orgId, normalizedEmail, seatType], ); @@ -2835,9 +2836,9 @@ export function createOrganizationsRouter(): Router { ); const stagingKey = `direct_${orgId}_${targetUserId}`; await query( - `INSERT INTO invitation_seat_types (workos_invitation_id, workos_organization_id, email, seat_type) - VALUES ($1, $2, $3, $4) - ON CONFLICT (workos_invitation_id) DO UPDATE SET seat_type = EXCLUDED.seat_type`, + `INSERT INTO invitation_seat_types (workos_invitation_id, workos_organization_id, email, seat_type, source) + VALUES ($1, $2, $3, $4, 'admin_added') + ON CONFLICT (workos_invitation_id) DO UPDATE SET seat_type = EXCLUDED.seat_type, source = EXCLUDED.source`, [stagingKey, orgId, normalizedEmail, seatType], ); diff --git a/server/src/routes/workos-webhooks.ts b/server/src/routes/workos-webhooks.ts index 630525e93d..b9c6c25b0b 100644 --- a/server/src/routes/workos-webhooks.ts +++ b/server/src/routes/workos-webhooks.ts @@ -122,10 +122,14 @@ async function upsertMembership( const role = membership.role?.slug || 'member'; - // Consume any pending seat_type assignment from the invitation - const consumedSeatType = await consumeInvitationSeatType(membership.organization_id, userData.email); - const hasExplicitSeatType = consumedSeatType !== null; - const seatType = consumedSeatType || 'community_only'; + // Consume any pending seat_type + provisioning_source staged by the + // endpoint that triggered this membership creation. Falls back to defaults + // when no row was staged (e.g. someone added the membership directly in + // WorkOS rather than through one of our endpoints). + const consumed = await consumeInvitationSeatType(membership.organization_id, userData.email); + const hasExplicitSeatType = consumed !== null; + const seatType = consumed?.seat_type || 'community_only'; + const provisioningSource = consumed?.source || 'webhook'; const { assigned_role } = await upsertOrganizationMembership({ user_id: membership.user_id, @@ -137,6 +141,7 @@ async function upsertMembership( role, seat_type: seatType, has_explicit_seat_type: hasExplicitSeatType, + provisioning_source: provisioningSource, }); // If the DB promoted this member to owner, sync the change to WorkOS diff --git a/server/tests/integration/membership-webhook.test.ts b/server/tests/integration/membership-webhook.test.ts index 1e8982fed2..781291793f 100644 --- a/server/tests/integration/membership-webhook.test.ts +++ b/server/tests/integration/membership-webhook.test.ts @@ -264,15 +264,15 @@ describe('Membership webhook DB operations', () => { // ========================================================================= describe('consumeInvitationSeatType', () => { - it('returns and deletes the pending seat type', async () => { + it('returns and deletes the pending seat type and source', async () => { await pool.query( - `INSERT INTO invitation_seat_types (workos_invitation_id, workos_organization_id, email, seat_type) - VALUES ($1, $2, $3, $4)`, - ['inv_test_1', TEST_ORG_ID, 'invited@test.com', 'contributor'], + `INSERT INTO invitation_seat_types (workos_invitation_id, workos_organization_id, email, seat_type, source) + VALUES ($1, $2, $3, $4, $5)`, + ['inv_test_1', TEST_ORG_ID, 'invited@test.com', 'contributor', 'invited'], ); const result = await consumeInvitationSeatType(TEST_ORG_ID, 'invited@test.com'); - expect(result).toBe('contributor'); + expect(result).toEqual({ seat_type: 'contributor', source: 'invited' }); // Should be consumed (deleted) const second = await consumeInvitationSeatType(TEST_ORG_ID, 'invited@test.com'); @@ -280,14 +280,27 @@ describe('Membership webhook DB operations', () => { }); it('matches case-insensitively', async () => { + await pool.query( + `INSERT INTO invitation_seat_types (workos_invitation_id, workos_organization_id, email, seat_type, source) + VALUES ($1, $2, $3, $4, $5)`, + ['inv_test_2', TEST_ORG_ID, 'CamelCase@Test.com', 'contributor', 'invited'], + ); + + const result = await consumeInvitationSeatType(TEST_ORG_ID, 'camelcase@test.com'); + expect(result?.seat_type).toBe('contributor'); + expect(result?.source).toBe('invited'); + }); + + it('returns null source when staging row predates the source column', async () => { + // Backward compatibility: rows written before migration 436 have NULL source. await pool.query( `INSERT INTO invitation_seat_types (workos_invitation_id, workos_organization_id, email, seat_type) VALUES ($1, $2, $3, $4)`, - ['inv_test_2', TEST_ORG_ID, 'CamelCase@Test.com', 'contributor'], + ['inv_test_legacy', TEST_ORG_ID, 'legacy@test.com', 'community_only'], ); - const result = await consumeInvitationSeatType(TEST_ORG_ID, 'camelcase@test.com'); - expect(result).toBe('contributor'); + const result = await consumeInvitationSeatType(TEST_ORG_ID, 'legacy@test.com'); + expect(result).toEqual({ seat_type: 'community_only', source: null }); }); it('returns null when no invitation exists', async () => { @@ -571,5 +584,85 @@ describe('Membership webhook DB operations', () => { `DELETE FROM organization_memberships WHERE workos_organization_id = 'org_personal_autolink_user'`, ); }); + + it('stages provisioning_source=verified_domain so the webhook can record it', async () => { + await seedOrgWithVerifiedDomain('autolink.com'); + const workos = makeWorkOSMock(); + + const result = await autoLinkByVerifiedDomain(workos, AUTOLINK_USER, 'matt@autolink.com'); + expect(result).not.toBeNull(); + + // The staging row should now exist for the org+email pair so the + // organization_membership.created webhook can consume it. + const staged = await pool.query<{ seat_type: string; source: string | null }>( + `SELECT seat_type, source FROM invitation_seat_types + WHERE workos_organization_id = $1 AND lower(email) = lower($2)`, + [TEST_AUTOLINK_ORG_ID, 'matt@autolink.com'], + ); + expect(staged.rows[0]).toBeDefined(); + expect(staged.rows[0].seat_type).toBe('community_only'); + expect(staged.rows[0].source).toBe('verified_domain'); + }); + }); + + describe('upsertOrganizationMembership provisioning_source', () => { + it('writes provisioning_source on insert', async () => { + await upsertOrganizationMembership({ + user_id: TEST_USER_1, + organization_id: TEST_ORG_ID, + membership_id: 'om_source_test', + email: 'src@test.com', + first_name: 'Src', + last_name: 'Test', + role: 'member', + seat_type: 'community_only', + has_explicit_seat_type: false, + provisioning_source: 'verified_domain', + }); + + const row = await pool.query<{ provisioning_source: string | null }>( + 'SELECT provisioning_source FROM organization_memberships WHERE workos_user_id = $1 AND workos_organization_id = $2', + [TEST_USER_1, TEST_ORG_ID], + ); + expect(row.rows[0].provisioning_source).toBe('verified_domain'); + }); + + it('preserves an existing provisioning_source on subsequent upserts', async () => { + // Initial insert tags the row. + await upsertOrganizationMembership({ + user_id: TEST_USER_1, + organization_id: TEST_ORG_ID, + membership_id: 'om_pres_1', + email: 'pres@test.com', + first_name: null, + last_name: null, + role: 'member', + seat_type: 'community_only', + has_explicit_seat_type: false, + provisioning_source: 'admin_added', + }); + + // Subsequent webhook upsert with a less-specific source must not overwrite. + await upsertOrganizationMembership({ + user_id: TEST_USER_1, + organization_id: TEST_ORG_ID, + membership_id: 'om_pres_1', + email: 'pres@test.com', + first_name: null, + last_name: null, + role: 'admin', + seat_type: 'community_only', + has_explicit_seat_type: false, + provisioning_source: 'webhook', + }); + + const row = await pool.query<{ provisioning_source: string | null; role: string }>( + 'SELECT provisioning_source, role FROM organization_memberships WHERE workos_user_id = $1 AND workos_organization_id = $2', + [TEST_USER_1, TEST_ORG_ID], + ); + expect(row.rows[0].provisioning_source).toBe('admin_added'); + // Role still updates normally. + expect(row.rows[0].role).toBe('admin'); + }); }); }); From 1e8ef9a689259992f812b8ae00460ca907810922 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 26 Apr 2026 12:24:57 -0400 Subject: [PATCH 2/8] deps: bump the minor-and-patch group across 1 directory with 13 updates (#3275) Bumps the minor-and-patch group with 13 updates in the / directory: | Package | From | To | | --- | --- | --- | | [@anthropic-ai/sdk](https://github.com/anthropics/anthropic-sdk-typescript) | `0.90.0` | `0.91.1` | | [@slack/bolt](https://github.com/slackapi/bolt-js) | `4.7.0` | `4.7.1` | | [@workos-inc/widgets](https://github.com/workos/widgets/tree/HEAD/packages/widgets) | `1.10.1` | `1.10.2` | | [express-rate-limit](https://github.com/express-rate-limit/express-rate-limit) | `8.3.2` | `8.4.1` | | [isomorphic-dompurify](https://github.com/kkomelin/isomorphic-dompurify) | `3.9.0` | `3.10.0` | | [posthog-node](https://github.com/PostHog/posthog-js/tree/HEAD/packages/node) | `5.29.2` | `5.30.4` | | [resend](https://github.com/resend/resend-node) | `6.12.0` | `6.12.2` | | [stripe](https://github.com/stripe/stripe-node) | `22.0.2` | `22.1.0` | | [svix](https://github.com/svix/svix-webhooks) | `1.90.0` | `1.92.2` | | [ajv](https://github.com/ajv-validator/ajv) | `8.18.0` | `8.20.0` | | [msw](https://github.com/mswjs/msw) | `2.13.4` | `2.13.6` | | [puppeteer](https://github.com/puppeteer/puppeteer) | `24.41.0` | `24.42.0` | | [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `4.1.4` | `4.1.5` | Updates `@anthropic-ai/sdk` from 0.90.0 to 0.91.1 - [Release notes](https://github.com/anthropics/anthropic-sdk-typescript/releases) - [Changelog](https://github.com/anthropics/anthropic-sdk-typescript/blob/main/CHANGELOG.md) - [Commits](https://github.com/anthropics/anthropic-sdk-typescript/compare/sdk-v0.90.0...sdk-v0.91.1) Updates `@slack/bolt` from 4.7.0 to 4.7.1 - [Release notes](https://github.com/slackapi/bolt-js/releases) - [Changelog](https://github.com/slackapi/bolt-js/blob/main/CHANGELOG.md) - [Commits](https://github.com/slackapi/bolt-js/compare/@slack/bolt@4.7.0...v4.7.1) Updates `@workos-inc/widgets` from 1.10.1 to 1.10.2 - [Commits](https://github.com/workos/widgets/commits/HEAD/packages/widgets) Updates `express-rate-limit` from 8.3.2 to 8.4.1 - [Release notes](https://github.com/express-rate-limit/express-rate-limit/releases) - [Commits](https://github.com/express-rate-limit/express-rate-limit/compare/v8.3.2...v8.4.1) Updates `isomorphic-dompurify` from 3.9.0 to 3.10.0 - [Release notes](https://github.com/kkomelin/isomorphic-dompurify/releases) - [Commits](https://github.com/kkomelin/isomorphic-dompurify/compare/3.9.0...3.10.0) Updates `posthog-node` from 5.29.2 to 5.30.4 - [Release notes](https://github.com/PostHog/posthog-js/releases) - [Changelog](https://github.com/PostHog/posthog-js/blob/main/packages/node/CHANGELOG.md) - [Commits](https://github.com/PostHog/posthog-js/commits/posthog-node@5.30.4/packages/node) Updates `resend` from 6.12.0 to 6.12.2 - [Release notes](https://github.com/resend/resend-node/releases) - [Commits](https://github.com/resend/resend-node/compare/v6.12.0...v6.12.2) Updates `stripe` from 22.0.2 to 22.1.0 - [Release notes](https://github.com/stripe/stripe-node/releases) - [Changelog](https://github.com/stripe/stripe-node/blob/master/CHANGELOG.md) - [Commits](https://github.com/stripe/stripe-node/compare/v22.0.2...v22.1.0) Updates `svix` from 1.90.0 to 1.92.2 - [Release notes](https://github.com/svix/svix-webhooks/releases) - [Changelog](https://github.com/svix/svix-webhooks/blob/main/ChangeLog.md) - [Commits](https://github.com/svix/svix-webhooks/compare/v1.90.0...v1.92.2) Updates `ajv` from 8.18.0 to 8.20.0 - [Release notes](https://github.com/ajv-validator/ajv/releases) - [Commits](https://github.com/ajv-validator/ajv/compare/v8.18.0...v8.20.0) Updates `msw` from 2.13.4 to 2.13.6 - [Release notes](https://github.com/mswjs/msw/releases) - [Changelog](https://github.com/mswjs/msw/blob/main/CHANGELOG.md) - [Commits](https://github.com/mswjs/msw/compare/v2.13.4...v2.13.6) Updates `puppeteer` from 24.41.0 to 24.42.0 - [Release notes](https://github.com/puppeteer/puppeteer/releases) - [Changelog](https://github.com/puppeteer/puppeteer/blob/main/CHANGELOG.md) - [Commits](https://github.com/puppeteer/puppeteer/compare/puppeteer-v24.41.0...puppeteer-v24.42.0) Updates `vitest` from 4.1.4 to 4.1.5 - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.5/packages/vitest) --- updated-dependencies: - dependency-name: "@anthropic-ai/sdk" dependency-version: 0.91.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-and-patch - dependency-name: "@slack/bolt" dependency-version: 4.7.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-and-patch - dependency-name: "@workos-inc/widgets" dependency-version: 1.10.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-and-patch - dependency-name: express-rate-limit dependency-version: 8.4.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-and-patch - dependency-name: isomorphic-dompurify dependency-version: 3.10.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-and-patch - dependency-name: posthog-node dependency-version: 5.30.4 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-and-patch - dependency-name: resend dependency-version: 6.12.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-and-patch - dependency-name: stripe dependency-version: 22.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-and-patch - dependency-name: svix dependency-version: 1.92.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-and-patch - dependency-name: ajv dependency-version: 8.20.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-and-patch - dependency-name: msw dependency-version: 2.13.6 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: minor-and-patch - dependency-name: puppeteer dependency-version: 24.42.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-and-patch - dependency-name: vitest dependency-version: 4.1.5 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: minor-and-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Brian O'Kelley --- package-lock.json | 452 ++++++++++++++++++++++++---------------------- package.json | 26 +-- 2 files changed, 248 insertions(+), 230 deletions(-) diff --git a/package-lock.json b/package-lock.json index 186cdd4f61..7adc46039b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "3.0.0", "dependencies": { "@adcp/client": "5.19.0", - "@anthropic-ai/sdk": "^0.90.0", + "@anthropic-ai/sdk": "^0.91.1", "@asteasolutions/zod-to-openapi": "^8.5.0", "@contentauth/c2pa-node": "^0.5.4", "@google/generative-ai": "^0.24.1", @@ -20,22 +20,22 @@ "@opentelemetry/resources": "^2.7.0", "@opentelemetry/sdk-logs": "^0.215.0", "@opentelemetry/semantic-conventions": "^1.40.0", - "@slack/bolt": "^4.6.0", + "@slack/bolt": "^4.7.1", "@slack/web-api": "^7.15.1", "@types/jsonwebtoken": "^9.0.10", "@workos-inc/node": "^8.12.1", - "@workos-inc/widgets": "^1.10.1", + "@workos-inc/widgets": "^1.10.2", "axios": "^1.13.6", "canonicalize": "3.0.0", "cookie-parser": "^1.4.7", "csv-parse": "^6.2.1", "dotenv": "^17.4.2", "express": "^5.2.1", - "express-rate-limit": "^8.3.1", + "express-rate-limit": "^8.4.1", "file-type": "^22.0.1", "free-email-domains": "^1.2.26", "googleapis": "^171.4.0", - "isomorphic-dompurify": "^3.9.0", + "isomorphic-dompurify": "^3.10.0", "jose": "^6.2.2", "jsonwebtoken": "^9.0.3", "linkedom": "^0.18.12", @@ -46,13 +46,13 @@ "pg": "^8.20.0", "pino": "^10.3.1", "pino-pretty": "^13.1.3", - "posthog-node": "^5.29.2", - "resend": "^6.12.0", + "posthog-node": "^5.30.4", + "resend": "^6.12.2", "rss-parser": "^3.13.0", "semver": "^7.7.2", "sharp": "^0.34.5", - "stripe": "^22.0.2", - "svix": "^1.89.0", + "stripe": "^22.1.0", + "svix": "^1.92.2", "whoiser": "^2.0.0-beta.10", "yaml": "^2.8.3", "yauzl": "^3.2.1", @@ -70,7 +70,7 @@ "@types/semver": "^7.7.1", "@types/supertest": "^7.2.0", "@types/yauzl": "^2.10.3", - "ajv": "^8.18.0", + "ajv": "^8.20.0", "ajv-formats": "^3.0.1", "c8": "^11.0.0", "concurrently": "^9.2.1", @@ -78,13 +78,13 @@ "husky": "^9.1.7", "jsdom": "^29.0.1", "mintlify": "^4.2.531", - "msw": "^2.13.4", - "puppeteer": "^24.41.0", + "msw": "^2.13.6", + "puppeteer": "^24.42.0", "supertest": "^7.2.2", "tar": "^6.2.1", "tsx": "^4.21.0", "typescript": "~5.9.3", - "vitest": "^4.1.4" + "vitest": "^4.1.5" }, "engines": { "node": ">=22.22.0" @@ -204,9 +204,9 @@ } }, "node_modules/@anthropic-ai/sdk": { - "version": "0.90.0", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.90.0.tgz", - "integrity": "sha512-MzZtPabJF1b0FTDl6Z6H5ljphPwACLGP13lu8MTiB8jXaW/YXlpOp+Po2cVou3MPM5+f5toyLnul9whKCy7fBg==", + "version": "0.91.1", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.91.1.tgz", + "integrity": "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==", "license": "MIT", "dependencies": { "json-schema-to-ts": "^3.1.1" @@ -911,9 +911,9 @@ } }, "node_modules/@emnapi/core": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", - "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", "dev": true, "license": "MIT", "optional": true, @@ -931,9 +931,9 @@ "optional": true }, "node_modules/@emnapi/runtime": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", - "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", "license": "MIT", "optional": true, "dependencies": { @@ -5234,9 +5234,9 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz", - "integrity": "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", "dev": true, "license": "MIT", "optional": true, @@ -5507,9 +5507,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.124.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz", - "integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==", + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", + "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", "dev": true, "license": "MIT", "funding": { @@ -5533,9 +5533,18 @@ "license": "MIT" }, "node_modules/@posthog/core": { - "version": "1.25.2", - "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.25.2.tgz", - "integrity": "sha512-h2FO7ut/BbfwpAXWpwdDHTzQgUo9ibDFEs6ZO+3cI3KPWQt5XwczK1OLAuPprcjm8T/jl0SH8jSFo5XdU4RbTg==", + "version": "1.27.5", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.27.5.tgz", + "integrity": "sha512-sYCcUDuYKumYTjwGqGCPT8aUy086v9PKw5wD+UXCRSfCsxWy5R/ic6W13kGTn4O5B2cD1V19wJv19oIH5kHUiQ==", + "license": "MIT", + "dependencies": { + "@posthog/types": "1.372.1" + } + }, + "node_modules/@posthog/types": { + "version": "1.372.1", + "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.372.1.tgz", + "integrity": "sha512-yl2x2HgtdhFk8bvf6HuRSDzXnKmKGrzNxUahKvA/0mcwheweINvmWy5MsN55NevrcCrNXA6m8GPHS9o/y1mn4A==", "license": "MIT" }, "node_modules/@protobufjs/aspromise": { @@ -7194,9 +7203,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz", - "integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==", "cpu": [ "arm64" ], @@ -7211,9 +7220,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz", - "integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==", "cpu": [ "arm64" ], @@ -7228,9 +7237,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz", - "integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==", "cpu": [ "x64" ], @@ -7245,9 +7254,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz", - "integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==", "cpu": [ "x64" ], @@ -7262,9 +7271,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz", - "integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz", + "integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==", "cpu": [ "arm" ], @@ -7279,9 +7288,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz", - "integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==", "cpu": [ "arm64" ], @@ -7296,9 +7305,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz", - "integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==", "cpu": [ "arm64" ], @@ -7313,9 +7322,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz", - "integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==", "cpu": [ "ppc64" ], @@ -7330,9 +7339,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz", - "integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==", "cpu": [ "s390x" ], @@ -7347,9 +7356,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz", - "integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==", "cpu": [ "x64" ], @@ -7364,9 +7373,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz", - "integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==", "cpu": [ "x64" ], @@ -7381,9 +7390,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz", - "integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==", "cpu": [ "arm64" ], @@ -7398,9 +7407,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz", - "integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz", + "integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==", "cpu": [ "wasm32" ], @@ -7408,18 +7417,18 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "1.9.2", - "@emnapi/runtime": "1.9.2", - "@napi-rs/wasm-runtime": "^1.1.3" + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" }, "engines": { - "node": ">=14.0.0" + "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz", - "integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==", "cpu": [ "arm64" ], @@ -7434,9 +7443,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz", - "integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==", "cpu": [ "x64" ], @@ -7451,9 +7460,9 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz", - "integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz", + "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==", "dev": true, "license": "MIT" }, @@ -7628,16 +7637,16 @@ } }, "node_modules/@slack/bolt": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@slack/bolt/-/bolt-4.7.0.tgz", - "integrity": "sha512-Xpf+gKegNvkHpft1z4YiuqZdciJ3tUp1bIRQxylW30Ovf+hzjb0M1zTHVtJsRw9jsjPxHTPoyanEXVvG6qVE1g==", + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/@slack/bolt/-/bolt-4.7.1.tgz", + "integrity": "sha512-CIyVjvHm/gY/e6n/xsJibcQFh2+S0WrlaV4LzpwXDlsmWuDrhLzAqBcOP/i9vgyFklO+DXD9Pzbz2uSPCctnZQ==", "license": "MIT", "dependencies": { "@slack/logger": "^4.0.1", "@slack/oauth": "^3.0.5", "@slack/socket-mode": "^2.0.6", "@slack/types": "^2.20.1", - "@slack/web-api": "^7.15.0", + "@slack/web-api": "^7.15.1", "axios": "^1.12.0", "express": "^5.0.0", "path-to-regexp": "^8.1.0", @@ -8797,16 +8806,16 @@ "license": "ISC" }, "node_modules/@vitest/expect": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.4.tgz", - "integrity": "sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.5.tgz", + "integrity": "sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.4", - "@vitest/utils": "4.1.4", + "@vitest/spy": "4.1.5", + "@vitest/utils": "4.1.5", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -8815,13 +8824,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.4.tgz", - "integrity": "sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.5.tgz", + "integrity": "sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.4", + "@vitest/spy": "4.1.5", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -8842,9 +8851,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.4.tgz", - "integrity": "sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.5.tgz", + "integrity": "sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==", "dev": true, "license": "MIT", "dependencies": { @@ -8855,13 +8864,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.4.tgz", - "integrity": "sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.5.tgz", + "integrity": "sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.4", + "@vitest/utils": "4.1.5", "pathe": "^2.0.3" }, "funding": { @@ -8869,14 +8878,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.4.tgz", - "integrity": "sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.5.tgz", + "integrity": "sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.4", - "@vitest/utils": "4.1.4", + "@vitest/pretty-format": "4.1.5", + "@vitest/utils": "4.1.5", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -8885,9 +8894,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.4.tgz", - "integrity": "sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.5.tgz", + "integrity": "sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==", "dev": true, "license": "MIT", "funding": { @@ -8895,13 +8904,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.4.tgz", - "integrity": "sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.5.tgz", + "integrity": "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.4", + "@vitest/pretty-format": "4.1.5", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -8922,9 +8931,9 @@ } }, "node_modules/@workos-inc/widgets": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@workos-inc/widgets/-/widgets-1.10.1.tgz", - "integrity": "sha512-48/Z8/+2qeMwzXtVRKsI+dLtI+ZvecCdk48E8GPJevh50LwklcuhxyiVfeJVIMuOAe+ikytBYvToHtATSturGg==", + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@workos-inc/widgets/-/widgets-1.10.2.tgz", + "integrity": "sha512-g/iwbhw9igFWTz+WIFXeX5GsjRn/GgXFXRbfIVuOy5JcH8TWdbtrdJASnaPJ/kxhqVfFxqG1bpr0xYS1h3Z0iQ==", "license": "MIT", "dependencies": { "@radix-ui/react-icons": "^1.3.2", @@ -9051,9 +9060,9 @@ } }, "node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -11385,9 +11394,9 @@ } }, "node_modules/dompurify": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.0.tgz", - "integrity": "sha512-nolgK9JcaUXMSmW+j1yaSvaEaoXYHwWyGJlkoCTghc97KgGDDSnpoU/PlEnw63Ah+TGKFOyY+X5LnxaWbCSfXg==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.1.tgz", + "integrity": "sha512-JahakDAIg1gyOm7dlgWSDjV4n7Ip2PKR55NIT6jrMfIgLFgWo81vdr1/QGqWtFNRqXP9UV71oVePtjqS2ebnPw==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -12225,9 +12234,9 @@ } }, "node_modules/express-rate-limit": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz", - "integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.4.1.tgz", + "integrity": "sha512-NGVYwQSAyEQgzxX1iCM978PP9AdO/hW93gMcF6ZwQCm+rFvLsBH6w4xcXWTcliS8La5EPRN3p9wzItqBwJrfNw==", "license": "MIT", "dependencies": { "ip-address": "10.1.0" @@ -15405,12 +15414,12 @@ "license": "ISC" }, "node_modules/isomorphic-dompurify": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/isomorphic-dompurify/-/isomorphic-dompurify-3.9.0.tgz", - "integrity": "sha512-3REwJAnIqjWO7qbfyKWMBI7hUHFhqUor7weFG3WbJW6Enq3V7cx60QuurWjGUXxbX57Iy4RJIkAZFObAqiqpxw==", + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/isomorphic-dompurify/-/isomorphic-dompurify-3.10.0.tgz", + "integrity": "sha512-Gj2duy4dACsP/FLPvwJ3+MXTlGtOo+O4yfpA0jdxuz/sZlbZzazGzScajOHRwH7PCy4j3bh5ibLGJY4/Rb5kGQ==", "license": "MIT", "dependencies": { - "dompurify": "^3.4.0", + "dompurify": "^3.4.1", "jsdom": "^29.0.2" }, "engines": { @@ -17712,9 +17721,9 @@ "license": "MIT" }, "node_modules/msw": { - "version": "2.13.4", - "resolved": "https://registry.npmjs.org/msw/-/msw-2.13.4.tgz", - "integrity": "sha512-fPlKBeFe+8rpcyR3umUmmHuNwu6gc6T3STvkgEa9WDX/HEgal9wDeflpCUAIRtmvaLZM2igfI5y1bZ9G5J26KA==", + "version": "2.13.6", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.13.6.tgz", + "integrity": "sha512-GAJbQy8Ra/Ydjt0Hb2MGT2qhzd83J3+QZMHdH85uW7r/XkKc846+Ma2PLif5hGvTm5Yqa+wkcstpim0WeLZU9g==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -19337,12 +19346,12 @@ } }, "node_modules/posthog-node": { - "version": "5.29.2", - "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.29.2.tgz", - "integrity": "sha512-rI7kkF0XqDc0G1qjx+Hb4iuY9NAlL+XQNoGOpnEpRNTUcXvjY6WlsRGZ9m2whgc39emrrYdszi/YT8wZkr2xsg==", + "version": "5.30.4", + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.30.4.tgz", + "integrity": "sha512-NjEgLdQwff9aPssHgmDYYDe5EbMsR33k327g/hC7RM3qbaqWio559TqAaVjP+Ks8c61liP0Vhr90lr4dpO48OQ==", "license": "MIT", "dependencies": { - "@posthog/core": "1.25.2" + "@posthog/core": "1.27.5" }, "engines": { "node": "^20.20.0 || >=22.22.0" @@ -19612,9 +19621,9 @@ } }, "node_modules/puppeteer": { - "version": "24.41.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.41.0.tgz", - "integrity": "sha512-W6Fk0J3TPjjtwjXOyR/qf+YaL0H/Uq8HIgHcXG4mNM/IgbKMCH/HPyK0Fi2qbTU/QpSl9bCte2yBpGHKejTpIw==", + "version": "24.42.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.42.0.tgz", + "integrity": "sha512-94MoPfFp2eY3eYIMdINkez4IOP5TMHntlZbVx06fHlQTtiQiYgaY0L2Zzfod8PVUkPqP7m3Qlre2v8YS8cudPA==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -19623,7 +19632,7 @@ "chromium-bidi": "14.0.0", "cosmiconfig": "^9.0.0", "devtools-protocol": "0.0.1595872", - "puppeteer-core": "24.41.0", + "puppeteer-core": "24.42.0", "typed-query-selector": "^2.12.1" }, "bin": { @@ -19634,9 +19643,9 @@ } }, "node_modules/puppeteer-core": { - "version": "24.41.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.41.0.tgz", - "integrity": "sha512-rLIUri7E/NQ3APSEYCCozaSJx0u8Tu9wxO6BJwnvXmIgILSK3L0TombaVh3izp1njAGrO6H2ru0hcIrLF+gWLw==", + "version": "24.42.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.42.0.tgz", + "integrity": "sha512-T4zXokk/izH01fYPhyyev1A4piWiOKrYq7CUFpdoYQxmOnXoV6YjUabmfIjCYkNspSoAXIxRid3Tw+Vg0fthYg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -20492,9 +20501,9 @@ } }, "node_modules/resend": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/resend/-/resend-6.12.0.tgz", - "integrity": "sha512-CaxEvX1z+/MGbgnhsM/bvmkbnZd1v1sEXELAjBNSDBQNMaB7MgqOyrBgI27CYikEgdaDnBrXghAXYWTBs/h5Bw==", + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/resend/-/resend-6.12.2.tgz", + "integrity": "sha512-xwgmU4b0OqoabJsIoK/x0Whk0Fcs3bpbK4i/DEWPiE5hYJHyHl0TbB6QbI3gIr+bLdLUJ1GYm/fe41aVFuHXgw==", "license": "MIT", "dependencies": { "postal-mime": "2.7.4", @@ -20512,6 +20521,29 @@ } } }, + "node_modules/resend/node_modules/svix": { + "version": "1.90.0", + "resolved": "https://registry.npmjs.org/svix/-/svix-1.90.0.tgz", + "integrity": "sha512-ljkZuyy2+IBEoESkIpn8sLM+sxJHQcPxlZFxU+nVDhltNfUMisMBzWX/UR8SjEnzoI28ZjCzMbmYAPwSTucoMw==", + "license": "MIT", + "dependencies": { + "standardwebhooks": "1.0.0", + "uuid": "^10.0.0" + } + }, + "node_modules/resend/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/resolve": { "version": "1.22.12", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", @@ -20756,14 +20788,14 @@ } }, "node_modules/rolldown": { - "version": "1.0.0-rc.15", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz", - "integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==", + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz", + "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.124.0", - "@rolldown/pluginutils": "1.0.0-rc.15" + "@oxc-project/types": "=0.127.0", + "@rolldown/pluginutils": "1.0.0-rc.17" }, "bin": { "rolldown": "bin/cli.mjs" @@ -20772,21 +20804,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.15", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.15", - "@rolldown/binding-darwin-x64": "1.0.0-rc.15", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.15", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" + "@rolldown/binding-android-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-x64": "1.0.0-rc.17", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" } }, "node_modules/router": { @@ -22011,9 +22043,9 @@ } }, "node_modules/stripe": { - "version": "22.0.2", - "resolved": "https://registry.npmjs.org/stripe/-/stripe-22.0.2.tgz", - "integrity": "sha512-2/BLrQ3oB1zlNfeL/LfHFjTGx6EQn0j+ztrrTJHuDjV5VVIpk92oSDaxyKLUr3pG3dnee2LZqhFUv2Bf0G1/3g==", + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-22.1.0.tgz", + "integrity": "sha512-w/xHyJGxXWnLPbNHG13sz/fae0MrFGC80Oz7YbICQymbfpqfEcsoG+6yG+9BWb81PWc4rrkeSO4wmTcmefmbLw==", "license": "MIT", "engines": { "node": ">=18" @@ -22182,26 +22214,12 @@ } }, "node_modules/svix": { - "version": "1.90.0", - "resolved": "https://registry.npmjs.org/svix/-/svix-1.90.0.tgz", - "integrity": "sha512-ljkZuyy2+IBEoESkIpn8sLM+sxJHQcPxlZFxU+nVDhltNfUMisMBzWX/UR8SjEnzoI28ZjCzMbmYAPwSTucoMw==", + "version": "1.92.2", + "resolved": "https://registry.npmjs.org/svix/-/svix-1.92.2.tgz", + "integrity": "sha512-ZmuA3UVvlnF9EgxlzmPtF7CKjQb64Z6OFlyfdDfU0sdcC7dJa+3aOYX5B9mA+RS6ch1AxBa4UP/l6KmqfGtWBQ==", "license": "MIT", "dependencies": { - "standardwebhooks": "1.0.0", - "uuid": "^10.0.0" - } - }, - "node_modules/svix/node_modules/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" + "standardwebhooks": "1.0.0" } }, "node_modules/symbol-tree": { @@ -22893,9 +22911,9 @@ } }, "node_modules/typed-query-selector": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.1.tgz", - "integrity": "sha512-uzR+FzI8qrUEIu96oaeBJmd9E7CFEiQ3goA5qCVgc4s5llSubcfGHq9yUstZx/k4s9dXHVKsE35YWoFyvEqEHA==", + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", + "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", "dev": true, "license": "MIT" }, @@ -23465,17 +23483,17 @@ } }, "node_modules/vite": { - "version": "8.0.8", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz", - "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==", + "version": "8.0.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz", + "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", - "postcss": "^8.5.8", - "rolldown": "1.0.0-rc.15", - "tinyglobby": "^0.2.15" + "postcss": "^8.5.10", + "rolldown": "1.0.0-rc.17", + "tinyglobby": "^0.2.16" }, "bin": { "vite": "bin/vite.js" @@ -23556,9 +23574,9 @@ } }, "node_modules/vite/node_modules/postcss": { - "version": "8.5.9", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", - "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", + "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", "dev": true, "funding": [ { @@ -23585,19 +23603,19 @@ } }, "node_modules/vitest": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.4.tgz", - "integrity": "sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.5.tgz", + "integrity": "sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.4", - "@vitest/mocker": "4.1.4", - "@vitest/pretty-format": "4.1.4", - "@vitest/runner": "4.1.4", - "@vitest/snapshot": "4.1.4", - "@vitest/spy": "4.1.4", - "@vitest/utils": "4.1.4", + "@vitest/expect": "4.1.5", + "@vitest/mocker": "4.1.5", + "@vitest/pretty-format": "4.1.5", + "@vitest/runner": "4.1.5", + "@vitest/snapshot": "4.1.5", + "@vitest/spy": "4.1.5", + "@vitest/utils": "4.1.5", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -23625,12 +23643,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.4", - "@vitest/browser-preview": "4.1.4", - "@vitest/browser-webdriverio": "4.1.4", - "@vitest/coverage-istanbul": "4.1.4", - "@vitest/coverage-v8": "4.1.4", - "@vitest/ui": "4.1.4", + "@vitest/browser-playwright": "4.1.5", + "@vitest/browser-preview": "4.1.5", + "@vitest/browser-webdriverio": "4.1.5", + "@vitest/coverage-istanbul": "4.1.5", + "@vitest/coverage-v8": "4.1.5", + "@vitest/ui": "4.1.5", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" diff --git a/package.json b/package.json index a41ebfd36c..d446fe3922 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ }, "dependencies": { "@adcp/client": "5.19.0", - "@anthropic-ai/sdk": "^0.90.0", + "@anthropic-ai/sdk": "^0.91.1", "@asteasolutions/zod-to-openapi": "^8.5.0", "@contentauth/c2pa-node": "^0.5.4", "@google/generative-ai": "^0.24.1", @@ -83,22 +83,22 @@ "@opentelemetry/resources": "^2.7.0", "@opentelemetry/sdk-logs": "^0.215.0", "@opentelemetry/semantic-conventions": "^1.40.0", - "@slack/bolt": "^4.6.0", + "@slack/bolt": "^4.7.1", "@slack/web-api": "^7.15.1", "@types/jsonwebtoken": "^9.0.10", "@workos-inc/node": "^8.12.1", - "@workos-inc/widgets": "^1.10.1", + "@workos-inc/widgets": "^1.10.2", "axios": "^1.13.6", "canonicalize": "3.0.0", "cookie-parser": "^1.4.7", "csv-parse": "^6.2.1", "dotenv": "^17.4.2", "express": "^5.2.1", - "express-rate-limit": "^8.3.1", + "express-rate-limit": "^8.4.1", "file-type": "^22.0.1", "free-email-domains": "^1.2.26", "googleapis": "^171.4.0", - "isomorphic-dompurify": "^3.9.0", + "isomorphic-dompurify": "^3.10.0", "jose": "^6.2.2", "jsonwebtoken": "^9.0.3", "linkedom": "^0.18.12", @@ -109,13 +109,13 @@ "pg": "^8.20.0", "pino": "^10.3.1", "pino-pretty": "^13.1.3", - "posthog-node": "^5.29.2", - "resend": "^6.12.0", + "posthog-node": "^5.30.4", + "resend": "^6.12.2", "rss-parser": "^3.13.0", "semver": "^7.7.2", "sharp": "^0.34.5", - "stripe": "^22.0.2", - "svix": "^1.89.0", + "stripe": "^22.1.0", + "svix": "^1.92.2", "whoiser": "^2.0.0-beta.10", "yaml": "^2.8.3", "yauzl": "^3.2.1", @@ -133,7 +133,7 @@ "@types/semver": "^7.7.1", "@types/supertest": "^7.2.0", "@types/yauzl": "^2.10.3", - "ajv": "^8.18.0", + "ajv": "^8.20.0", "ajv-formats": "^3.0.1", "c8": "^11.0.0", "concurrently": "^9.2.1", @@ -141,13 +141,13 @@ "husky": "^9.1.7", "jsdom": "^29.0.1", "mintlify": "^4.2.531", - "msw": "^2.13.4", - "puppeteer": "^24.41.0", + "msw": "^2.13.6", + "puppeteer": "^24.42.0", "supertest": "^7.2.2", "tar": "^6.2.1", "tsx": "^4.21.0", "typescript": "~5.9.3", - "vitest": "^4.1.4" + "vitest": "^4.1.5" }, "engines": { "node": ">=22.22.0" From 84cb8eac2d2ee6476b61bf235f0a53fe4d4d843b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 26 Apr 2026 12:25:00 -0400 Subject: [PATCH 3/8] ci: Bump actions/create-github-app-token from 2 to 3 (#3260) Bumps [actions/create-github-app-token](https://github.com/actions/create-github-app-token) from 2 to 3. - [Release notes](https://github.com/actions/create-github-app-token/releases) - [Commits](https://github.com/actions/create-github-app-token/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/create-github-app-token dependency-version: '3' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Brian O'Kelley --- .github/workflows/ipr-check-callable.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ipr-check-callable.yml b/.github/workflows/ipr-check-callable.yml index f1a2f9007c..84d59541fd 100644 --- a/.github/workflows/ipr-check-callable.yml +++ b/.github/workflows/ipr-check-callable.yml @@ -65,7 +65,7 @@ jobs: steps: - name: Mint AAO IPR Bot installation token (scoped to adcp) id: app-token - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 with: app-id: ${{ secrets.IPR_APP_ID }} private-key: ${{ secrets.IPR_APP_PRIVATE_KEY }} From 10e35b8c4ed65031c136b9be1ac9f9bdb311efcc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 26 Apr 2026 12:25:05 -0400 Subject: [PATCH 4/8] ci: Bump actions/checkout from 5 to 6 (#3261) Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Brian O'Kelley --- .github/workflows/validate-agent-context.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validate-agent-context.yml b/.github/workflows/validate-agent-context.yml index 8d1304b90d..5f1347eaf6 100644 --- a/.github/workflows/validate-agent-context.yml +++ b/.github/workflows/validate-agent-context.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 2 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: '22' From 3eeb2bd7241f1a289b3fd05682eced8d27e4f051 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 26 Apr 2026 12:26:14 -0400 Subject: [PATCH 5/8] ci: Bump actions/upload-artifact from 4 to 7 (#3262) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 7. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4...v7) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Brian O'Kelley --- .github/workflows/training-agent-storyboards.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/training-agent-storyboards.yml b/.github/workflows/training-agent-storyboards.yml index 7b4cfe914f..b9cf723ca9 100644 --- a/.github/workflows/training-agent-storyboards.yml +++ b/.github/workflows/training-agent-storyboards.yml @@ -148,7 +148,7 @@ jobs: - name: Upload storyboards log on failure if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: storyboards-log-${{ matrix.mode }} path: /tmp/storyboards.log @@ -202,7 +202,7 @@ jobs: - name: Persist matrix results for summary job if: success() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: storyboard-results-${{ matrix.mode }} path: /tmp/storyboard-results/results.txt From 9fd51dbd9468d25760994df8f8c2d36cc694d7f2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 26 Apr 2026 12:26:17 -0400 Subject: [PATCH 6/8] ci: Bump actions/download-artifact from 4 to 8 (#3263) Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 8. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v4...v8) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: '8' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Brian O'Kelley --- .github/workflows/training-agent-storyboards.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/training-agent-storyboards.yml b/.github/workflows/training-agent-storyboards.yml index b9cf723ca9..c364599b03 100644 --- a/.github/workflows/training-agent-storyboards.yml +++ b/.github/workflows/training-agent-storyboards.yml @@ -221,7 +221,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Download both matrix results - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: pattern: storyboard-results-* path: /tmp/storyboard-results From 1265e68eca8985d60396227c75da7261ff340823 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 26 Apr 2026 12:28:06 -0400 Subject: [PATCH 7/8] =?UTF-8?q?feat(addie):=20adoption=20prompt=20rules=20?= =?UTF-8?q?=E2=80=94=20company=20listing=20+=20team=20WG=20coverage=20(#32?= =?UTF-8?q?78)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(addie): adoption prompt rules — company listing + team WG coverage Stage 1.5 of #2299. Adds the two deferred adoption rules promised in the original Stage 1 thread. New MemberContext signals: - adoption.has_company_listing — derived from already-fetched member_profile (no new query) - adoption.team_wg_coverage — fraction (0–1) of org members in ≥1 working group, computed via one query against working_group_memberships using the WorkOS membership list we already fetch for member_count New rules: - org.owner_set_company_listing (priority 76): non-personal org owner without a public listing - org.owner_team_wg_coverage_low (priority 73): owner of a 5+ team with <30% WG coverage API key rule still deferred — adding it requires a cheaper signal source than a per-request WorkOS HTTP call. Co-Authored-By: Claude Opus 4.7 (1M context) * fixup: apply expert review — operator role, threshold, copy - Gate listing + team-WG rules on owner OR admin (not just owner) so day-to-day org operators see them - Lower team-WG coverage threshold from <30% → <50%; lower team size floor from >5 → >=3 so small teams with zero WG participation fire - Replace prompt copy with first-person verb-first style matching neighbouring rules ("Invite your team", "Complete my profile"): - "List my company in the directory" - "Find working groups for my team" - Add isOrgOperator helper, dev-mode coverage degradation comment - Tighten coverage predicate to (coverage ?? 1) < 0.5 Co-Authored-By: Claude Opus 4.7 (1M context) --------- Co-authored-by: Claude Opus 4.7 (1M context) --- .changeset/addie-adoption-prompt-rules.md | 9 +++ server/scripts/prompt-scenarios.ts | 8 +++ .../addie/home/builders/rules/prompt-rules.ts | 36 ++++++++++ server/src/addie/member-context.ts | 60 ++++++++++++++-- server/tests/unit/suggested-prompts.test.ts | 71 +++++++++++++++++++ 5 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 .changeset/addie-adoption-prompt-rules.md diff --git a/.changeset/addie-adoption-prompt-rules.md b/.changeset/addie-adoption-prompt-rules.md new file mode 100644 index 0000000000..dae4df4751 --- /dev/null +++ b/.changeset/addie-adoption-prompt-rules.md @@ -0,0 +1,9 @@ +--- +--- + +Stage 1.5 of persona-driven Addie suggested prompts (#2299): add the two deferred adoption rules. Adds `adoption.has_company_listing` (derived from existing fetched profile, free) and `adoption.team_wg_coverage` (one DB query against `working_group_memberships`, reusing the WorkOS membership list already fetched) to MemberContext. Two new rules: + +- **List my company in the directory** (priority 76): non-personal org with no public listing, fires for org owners and admins. +- **Find working groups for my team** (priority 73): owner/admin of a 3+ team with less than half in any working group. + +API key count rule is still deferred — needs a cheaper signal source than per-request WorkOS API calls. diff --git a/server/scripts/prompt-scenarios.ts b/server/scripts/prompt-scenarios.ts index 98e1a2fc4e..603d3b8138 100644 --- a/server/scripts/prompt-scenarios.ts +++ b/server/scripts/prompt-scenarios.ts @@ -65,6 +65,14 @@ show('Owner of 5-person team, Builder tier, profile incomplete', base({ org_membership: { role: 'owner', member_count: 5, joined_at: ago(120) }, community_profile: { is_public: false, slug: null, completeness: 50, github_username: null }, })); +show('Owner of company without a public listing', base({ + org_membership: { role: 'owner', member_count: 3, joined_at: ago(60) }, + adoption: { has_company_listing: false, team_wg_coverage: 0.5 }, +})); +show('Owner of 8-person team with low WG coverage', base({ + org_membership: { role: 'owner', member_count: 8, joined_at: ago(180) }, + adoption: { has_company_listing: true, team_wg_coverage: 0.1 }, +})); show('WG leader (Creative)', base({ working_groups: [{ name: 'Creative', is_leader: true }], })); diff --git a/server/src/addie/home/builders/rules/prompt-rules.ts b/server/src/addie/home/builders/rules/prompt-rules.ts index 3e6c9cce7c..7feaee7dbb 100644 --- a/server/src/addie/home/builders/rules/prompt-rules.ts +++ b/server/src/addie/home/builders/rules/prompt-rules.ts @@ -11,6 +11,16 @@ function isMember(ctx: MemberContext | null): boolean { return !!ctx?.is_member; } +/** + * True for users who can act on org-level setup (owner or admin in WorkOS). + * Day-to-day org operations at most companies are run by admins, not the + * original owner — gating only on 'owner' would miss them. + */ +function isOrgOperator(ctx: MemberContext | null): boolean { + const role = ctx?.org_membership?.role; + return role === 'owner' || role === 'admin'; +} + function isLinkedNonMember(ctx: MemberContext | null): boolean { return isMapped(ctx) && !isMember(ctx); } @@ -193,6 +203,32 @@ export const MEMBER_RULES: PromptRule[] = [ label: 'Invite your team', prompt: 'Help me invite my team to the organization.', }, + { + id: 'org.owner_set_company_listing', + priority: 76, + when: ({ memberContext }) => + isMember(memberContext) && + isOrgOperator(memberContext) && + memberContext?.organization?.is_personal === false && + memberContext?.adoption?.has_company_listing === false, + label: 'List my company in the directory', + prompt: 'Help me add my company to the directory.', + }, + { + id: 'org.owner_team_wg_coverage_low', + priority: 73, + when: ({ memberContext }) => { + if (!isMember(memberContext)) return false; + if (!isOrgOperator(memberContext)) return false; + // Fire for any team of 3+ where less than half are in working groups. + // Below 3 people, coverage math is too noisy to act on. + if ((memberContext?.org_membership?.member_count ?? 0) < 3) return false; + const coverage = memberContext?.adoption?.team_wg_coverage; + return (coverage ?? 1) < 0.5; + }, + label: 'Find working groups for my team', + prompt: 'Which working groups should my team join?', + }, { id: 'wg.find_groups', priority: 75, diff --git a/server/src/addie/member-context.ts b/server/src/addie/member-context.ts index 4941116b48..d9c7000b1f 100644 --- a/server/src/addie/member-context.ts +++ b/server/src/addie/member-context.ts @@ -102,6 +102,23 @@ async function getPendingContentForUser( return { total, by_committee: byCommittee }; } +/** + * Compute the fraction of org members who belong to ≥1 working group. + * Returns 0 when there are no org members in the input list. + */ +async function computeTeamWgCoverage(orgMemberUserIds: string[]): Promise { + if (orgMemberUserIds.length === 0) return 0; + const result = await query<{ count: string }>( + `SELECT COUNT(DISTINCT workos_user_id)::text as count + FROM working_group_memberships + WHERE workos_user_id = ANY($1::text[]) + AND status = 'active'`, + [orgMemberUserIds] + ); + const inWgs = parseInt(result.rows[0]?.count || '0', 10); + return inWgs / orgMemberUserIds.length; +} + /** * Fetch community profile data for a user. * Shared between getMemberContext (Slack) and getWebMemberContext (web chat). @@ -317,6 +334,14 @@ export interface MemberContext { /** Pending join requests for the organization (admins only) */ pending_join_requests_count?: number; + + /** Adoption signals for the organization (used by suggested-prompts rules) */ + adoption?: { + /** True when the org has a public company listing in the directory. */ + has_company_listing: boolean; + /** Fraction (0–1) of org members who belong to at least one working group. */ + team_wg_coverage: number; + }; } /** @@ -413,11 +438,13 @@ export async function getMemberContext(slackUserId: string): Promise m.userId).filter(Boolean); } catch (error) { logger.warn({ error, organizationId }, 'Addie: Failed to get org member count'); } @@ -519,6 +546,17 @@ export async function getMemberContext(slackUserId: string): Promise= 3. */ async function resolveContextFromLocalDb( context: MemberContext, organizationId: string, workosUserId: string, + orgMemberUserIds: string[] = [], ): Promise { const org = await orgDb.getOrganization(organizationId); if (org) { @@ -705,6 +745,16 @@ async function resolveContextFromLocalDb( }; } + try { + const teamWgCoverage = await computeTeamWgCoverage(orgMemberUserIds); + context.adoption = { + has_company_listing: !!(org && !org.is_personal && profile?.is_public), + team_wg_coverage: teamWgCoverage, + }; + } catch (error) { + logger.warn({ error, organizationId }, 'Addie Web: Failed to compute adoption signals'); + } + try { const subscriptionInfo = await orgDb.getSubscriptionInfo(organizationId); if (subscriptionInfo && subscriptionInfo.status !== 'none') { @@ -975,11 +1025,13 @@ export async function getWebMemberContext(workosUserId: string): Promise m.userId).filter(Boolean); } catch (error) { logger.warn({ error, organizationId }, 'Addie Web: Failed to get org member count'); } @@ -990,7 +1042,7 @@ export async function getWebMemberContext(workosUserId: string): Promise { }); expect(buildSuggestedPrompts(ctx, false).map((p) => p.label)).not.toContain('Invite your team'); }); + + it('shows List my company in the directory for non-personal owners without a public listing', () => { + const ctx = makeMember({ + org_membership: { role: 'owner', member_count: 3, joined_at: DAYS_AGO(60) }, + adoption: { has_company_listing: false, team_wg_coverage: 0.6 }, + }); + expect(buildSuggestedPrompts(ctx, false).map((p) => p.label)).toContain('List my company in the directory'); + }); + + it('also shows the listing prompt for org admins (not just owners)', () => { + const ctx = makeMember({ + org_membership: { role: 'admin', member_count: 3, joined_at: DAYS_AGO(60) }, + adoption: { has_company_listing: false, team_wg_coverage: 0.6 }, + }); + expect(buildSuggestedPrompts(ctx, false).map((p) => p.label)).toContain('List my company in the directory'); + }); + + it('does not show the listing prompt for plain members', () => { + const ctx = makeMember({ + org_membership: { role: 'member', member_count: 3, joined_at: DAYS_AGO(60) }, + adoption: { has_company_listing: false, team_wg_coverage: 0.6 }, + }); + expect(buildSuggestedPrompts(ctx, false).map((p) => p.label)).not.toContain('List my company in the directory'); + }); + + it('does not show listing prompt when listing already public', () => { + const ctx = makeMember({ + org_membership: { role: 'owner', member_count: 3, joined_at: DAYS_AGO(60) }, + adoption: { has_company_listing: true, team_wg_coverage: 0.6 }, + }); + expect(buildSuggestedPrompts(ctx, false).map((p) => p.label)).not.toContain('List my company in the directory'); + }); + + it('does not show listing prompt for personal orgs', () => { + const ctx = makeMember({ + organization: { + workos_organization_id: 'org_123', + name: 'Acme', + subscription_status: 'active', + is_personal: true, + membership_tier: 'individual_professional', + }, + org_membership: { role: 'owner', member_count: 1, joined_at: DAYS_AGO(60) }, + adoption: { has_company_listing: false, team_wg_coverage: 0 }, + }); + expect(buildSuggestedPrompts(ctx, false).map((p) => p.label)).not.toContain('List my company in the directory'); + }); + + it('shows Find working groups for my team when coverage is below 50% and team >= 3', () => { + const ctx = makeMember({ + org_membership: { role: 'owner', member_count: 4, joined_at: DAYS_AGO(60) }, + adoption: { has_company_listing: true, team_wg_coverage: 0.2 }, + }); + expect(buildSuggestedPrompts(ctx, false).map((p) => p.label)).toContain('Find working groups for my team'); + }); + + it('does not show team WG prompt when coverage is healthy (≥ 50%)', () => { + const ctx = makeMember({ + org_membership: { role: 'owner', member_count: 8, joined_at: DAYS_AGO(60) }, + adoption: { has_company_listing: true, team_wg_coverage: 0.6 }, + }); + expect(buildSuggestedPrompts(ctx, false).map((p) => p.label)).not.toContain('Find working groups for my team'); + }); + + it('does not show team WG prompt for tiny teams (< 3)', () => { + const ctx = makeMember({ + org_membership: { role: 'owner', member_count: 2, joined_at: DAYS_AGO(60) }, + adoption: { has_company_listing: true, team_wg_coverage: 0 }, + }); + expect(buildSuggestedPrompts(ctx, false).map((p) => p.label)).not.toContain('Find working groups for my team'); + }); }); describe('profile and working groups', () => { From 8474ae03e9641f3503dc50267cec316a0b24b50f Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 26 Apr 2026 15:54:28 -0400 Subject: [PATCH 8/8] feat(notifications): daily digest of new auto-provisioned members for org admins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consent receipt for the auto_provision_verified_domain=true default. With auto-add on, org membership grows quietly as verified-domain emails sign in — admins had no signal that their seat list was changing. This adds a daily Slack digest listing new auto-joined members per org and linking to the team page. Mechanics: - organizations.last_auto_provision_digest_sent_at watermark. - findOrgsWithNewAutoProvisionedMembers + listNewAutoProvisionedMembers find members where provisioning_source = 'verified_domain' and created_at > watermark. Skips personal workspaces and orgs with auto_provision_verified_domain = false. - scheduled/auto-provision-digest.ts runs daily (5min startup delay). Reuses sendToOrgAdmins from the seat-request reminder pattern — multi-admin orgs get group DM, single-admin orgs get direct DM. - Watermark only advances on successful Slack delivery, so unmapped admins or transient failures retry on the next run. Co-Authored-By: Claude Opus 4.7 (1M context) --- .changeset/auto-provision-digest.md | 23 ++ server/src/db/membership-db.ts | 111 ++++++++++ .../437_auto_provision_digest_sent_at.sql | 13 ++ server/src/http.ts | 10 + server/src/scheduled/auto-provision-digest.ts | 198 ++++++++++++++++++ .../integration/membership-webhook.test.ts | 140 +++++++++++++ 6 files changed, 495 insertions(+) create mode 100644 .changeset/auto-provision-digest.md create mode 100644 server/src/db/migrations/437_auto_provision_digest_sent_at.sql create mode 100644 server/src/scheduled/auto-provision-digest.ts diff --git a/.changeset/auto-provision-digest.md b/.changeset/auto-provision-digest.md new file mode 100644 index 0000000000..b61aa6782c --- /dev/null +++ b/.changeset/auto-provision-digest.md @@ -0,0 +1,23 @@ +--- +--- + +feat(notifications): daily digest of new auto-provisioned members for org admins + +The consent receipt for the `auto_provision_verified_domain` default. With auto-add on (which it is by default), org membership grows quietly when verified-domain emails sign in — owners had no signal that their seat list was changing. This adds a daily Slack digest that lists the new auto-joined members per org and links to the team page where the owner can review or flip the toggle off. + +## Mechanics + +- New per-org watermark `organizations.last_auto_provision_digest_sent_at`. Migration 437. +- New `findOrgsWithNewAutoProvisionedMembers()` and `listNewAutoProvisionedMembers(orgId, since)` queries find members where `provisioning_source = 'verified_domain'` and `created_at > watermark`. Skips personal workspaces and orgs with `auto_provision_verified_domain = false`. +- `server/src/scheduled/auto-provision-digest.ts` runs the check every 24 hours (5-minute startup delay so it doesn't fire during boot's noisy window). Uses the same Slack DM dispatch helper (`sendToOrgAdmins`) that the seat-request reminder job uses, so multi-admin orgs get a group DM and single-admin orgs get a direct DM. +- Watermark is only updated after a successful Slack delivery. If no admins are mapped to Slack, or delivery fails, the watermark stays put and the next run retries — eventually surfaces the news once an admin's Slack mapping shows up. + +## Tests + +- `server/tests/integration/membership-webhook.test.ts` — 31 → 37 tests. Six new cases cover: candidates filtered to verified-domain since watermark, opt-out flag honored, NULL watermark = beginning of time, members returned chronologically with non-verified-domain sources excluded, and `markAutoProvisionDigestSent` updating the timestamp. + +## Future + +- Email fallback for orgs without Slack mappings (defer — Slack is the primary admin surface today). +- Per-org cadence preference (defer — daily is the right default for a low-volume notification). +- "What's in this digest" preview accessible from the team page (defer — wire into the next admin UI cycle). diff --git a/server/src/db/membership-db.ts b/server/src/db/membership-db.ts index a1601bea52..50f7bce632 100644 --- a/server/src/db/membership-db.ts +++ b/server/src/db/membership-db.ts @@ -339,3 +339,114 @@ export async function autoLinkByVerifiedDomain( return null; } } + +// ── Auto-provision digest queries ─────────────────────────────────── + +/** + * Row in the auto-provision digest payload — one per newly-auto-joined member + * since the org's last digest watermark. + */ +export interface NewAutoProvisionedMember { + workos_user_id: string; + email: string; + first_name: string | null; + last_name: string | null; + role: string; + seat_type: string; + joined_at: Date; +} + +/** + * Find orgs that have at least one auto-provisioned member since their last + * digest watermark. Returns one row per org (with the org's owner emails and + * the count of new members) so the caller can iterate. + * + * Skip personal workspaces and orgs with auto_provision_verified_domain=false + * (the latter shouldn't have any verified-domain members anyway, but the join + * makes the intent explicit). + */ +export async function findOrgsWithNewAutoProvisionedMembers(): Promise< + Array<{ + workos_organization_id: string; + org_name: string; + last_sent_at: Date | null; + new_member_count: number; + }> +> { + const pool = getPool(); + const result = await pool.query<{ + workos_organization_id: string; + org_name: string; + last_sent_at: Date | null; + new_member_count: string; // pg COUNT comes back as string + }>(` + SELECT + o.workos_organization_id, + o.name AS org_name, + o.last_auto_provision_digest_sent_at AS last_sent_at, + COUNT(om.workos_user_id) AS new_member_count + FROM organizations o + JOIN organization_memberships om + ON om.workos_organization_id = o.workos_organization_id + WHERE om.provisioning_source = 'verified_domain' + AND om.created_at > COALESCE(o.last_auto_provision_digest_sent_at, 'epoch'::timestamptz) + AND COALESCE(o.is_personal, false) = false + AND COALESCE(o.auto_provision_verified_domain, true) = true + GROUP BY o.workos_organization_id, o.name, o.last_auto_provision_digest_sent_at + HAVING COUNT(om.workos_user_id) > 0 + `); + + return result.rows.map(r => ({ + workos_organization_id: r.workos_organization_id, + org_name: r.org_name, + last_sent_at: r.last_sent_at, + new_member_count: parseInt(r.new_member_count, 10), + })); +} + +/** + * List the auto-provisioned members added to a given org since the watermark. + * Used to build the digest body once findOrgsWithNewAutoProvisionedMembers has + * filtered to orgs with non-zero counts. + */ +export async function listNewAutoProvisionedMembers( + organizationId: string, + since: Date | null, +): Promise { + const pool = getPool(); + const sinceTs = since ?? new Date(0); + const result = await pool.query(` + SELECT + workos_user_id, + email, + first_name, + last_name, + role, + seat_type, + created_at AS joined_at + FROM organization_memberships + WHERE workos_organization_id = $1 + AND provisioning_source = 'verified_domain' + AND created_at > $2 + ORDER BY created_at ASC + `, [organizationId, sinceTs]); + + return result.rows; +} + +/** + * Mark the digest as sent for an organization. Called after successful delivery + * so the next run skips the same members. + */ +export async function markAutoProvisionDigestSent( + organizationId: string, + sentAt: Date = new Date(), +): Promise { + const pool = getPool(); + await pool.query( + `UPDATE organizations + SET last_auto_provision_digest_sent_at = $2, updated_at = NOW() + WHERE workos_organization_id = $1`, + [organizationId, sentAt], + ); +} diff --git a/server/src/db/migrations/437_auto_provision_digest_sent_at.sql b/server/src/db/migrations/437_auto_provision_digest_sent_at.sql new file mode 100644 index 0000000000..734b2c2753 --- /dev/null +++ b/server/src/db/migrations/437_auto_provision_digest_sent_at.sql @@ -0,0 +1,13 @@ +-- Track the last time the auto-provision new-member digest was sent for an +-- organization. The scheduled job uses this to find members auto-joined since +-- the previous digest (no per-member tracking, just a watermark per org). +-- +-- NULL means we've never sent — the first run after this migration deploys +-- will look back at the full window of `verified_domain` provisioning_source +-- members and include all of them in that org's first digest. + +ALTER TABLE organizations + ADD COLUMN IF NOT EXISTS last_auto_provision_digest_sent_at TIMESTAMPTZ; + +COMMENT ON COLUMN organizations.last_auto_provision_digest_sent_at IS + 'Watermark for the auto-provision new-member digest. Updated when the digest is successfully delivered. NULL = never sent.'; diff --git a/server/src/http.ts b/server/src/http.ts index 8a8f2871f3..57df78f609 100644 --- a/server/src/http.ts +++ b/server/src/http.ts @@ -8638,6 +8638,12 @@ ${p.category ? `${p.category}\n` : ''}${publishedUrl}< import('./scheduled/seat-request-reminders.js').then(({ startSeatRequestReminders }) => { startSeatRequestReminders(workos!); }).catch(err => logger.warn({ err }, 'Failed to start seat request reminders')); + + // Daily auto-provision new-member digest for org admins/owners. + // Consent receipt for the auto_provision_verified_domain default. + import('./scheduled/auto-provision-digest.js').then(({ startAutoProvisionDigest }) => { + startAutoProvisionDigest(workos!); + }).catch(err => logger.warn({ err }, 'Failed to start auto-provision digest')); } // Start Luma calendar sync (catches events missed by webhooks) @@ -8692,6 +8698,10 @@ ${p.category ? `${p.category}\n` : ''}${publishedUrl}< stopSeatRequestReminders(); }).catch(() => {}); + import('./scheduled/auto-provision-digest.js').then(({ stopAutoProvisionDigest }) => { + stopAutoProvisionDigest(); + }).catch(() => {}); + import('./luma/sync.js').then(({ stopLumaSync }) => { stopLumaSync(); }).catch(() => {}); diff --git a/server/src/scheduled/auto-provision-digest.ts b/server/src/scheduled/auto-provision-digest.ts new file mode 100644 index 0000000000..3e697c4789 --- /dev/null +++ b/server/src/scheduled/auto-provision-digest.ts @@ -0,0 +1,198 @@ +/** + * Daily digest of new auto-provisioned members for org owners/admins. + * + * Why this exists: with `auto_provision_verified_domain` defaulting to ON, + * the policy quietly grows org membership when a verified-domain email signs + * in. Without a notification, owners have no signal that their seat list is + * changing — exactly the SOC2-flavored finding the adtech-product reviewer + * flagged on the original PR. This is the consent receipt: every org that had + * auto-joined members since its last digest gets a Slack message listing + * them, with a link to the team page where the owner can review or flip the + * toggle off. + * + * Mechanics: + * - Watermark per org (`organizations.last_auto_provision_digest_sent_at`). + * - Once a day, find orgs with at least one verified-domain membership + * created since the watermark. Skip silently if zero. + * - Send a Slack DM/group-DM to the org's admin/owner cluster (matches the + * existing seat-request reminder dispatch pattern). + * - Mark the org as digested only after a successful send so failures retry. + */ + +import { WorkOS } from '@workos-inc/node'; +import { createLogger } from '../logger.js'; +import { + findOrgsWithNewAutoProvisionedMembers, + listNewAutoProvisionedMembers, + markAutoProvisionDigestSent, + type NewAutoProvisionedMember, +} from '../db/membership-db.js'; +import { sendToOrgAdmins, escapeSlackMrkdwn } from '../slack/org-group-dm.js'; +import { getOrgAdminEmails } from '../utils/org-admins.js'; + +const logger = createLogger('auto-provision-digest'); + +const APP_URL = process.env.APP_URL || 'https://agenticadvertising.org'; +// Run once a day. The watermark column is the per-org cooldown — a fresh +// candidate set with non-zero new members triggers a send for that org. +const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; +// Initial delay so the job doesn't fire during boot's noisy window. +const INITIAL_DELAY_MS = 5 * 60 * 1000; + +let intervalId: ReturnType | null = null; +let initialTimeoutId: ReturnType | null = null; + +export function startAutoProvisionDigest(workos: WorkOS): void { + if (intervalId || initialTimeoutId) return; + + initialTimeoutId = setTimeout(() => { + initialTimeoutId = null; + runDigest(workos).catch(err => + logger.error({ err }, 'Auto-provision digest failed'), + ); + intervalId = setInterval(() => { + runDigest(workos).catch(err => + logger.error({ err }, 'Auto-provision digest failed'), + ); + }, CHECK_INTERVAL_MS); + }, INITIAL_DELAY_MS); + + logger.info('Auto-provision digest scheduler started'); +} + +export function stopAutoProvisionDigest(): void { + if (initialTimeoutId) { + clearTimeout(initialTimeoutId); + initialTimeoutId = null; + } + if (intervalId) { + clearInterval(intervalId); + intervalId = null; + } +} + +/** + * One pass: find candidate orgs, send a digest to each, mark sent. + * Exported for tests + manual invocation from incident scripts. + */ +export async function runDigest(workos: WorkOS): Promise<{ + candidateOrgs: number; + delivered: number; + skipped: number; + failed: number; +}> { + const candidates = await findOrgsWithNewAutoProvisionedMembers(); + let delivered = 0; + let skipped = 0; + let failed = 0; + + for (const candidate of candidates) { + try { + const members = await listNewAutoProvisionedMembers( + candidate.workos_organization_id, + candidate.last_sent_at, + ); + if (members.length === 0) { + // Race: members were removed between findOrgs and list. Skip silently. + skipped++; + continue; + } + + const adminEmails = await getOrgAdminEmails(workos, candidate.workos_organization_id); + if (adminEmails.length === 0) { + // No admins/owners to notify. Don't update watermark — try again next + // run, in case an admin gets added. + logger.info( + { orgId: candidate.workos_organization_id, memberCount: members.length }, + 'Auto-provision digest: no admins to notify, deferring', + ); + skipped++; + continue; + } + + const message = buildSlackMessage(candidate.org_name, candidate.workos_organization_id, members); + const sent = await sendToOrgAdmins( + candidate.workos_organization_id, + adminEmails, + message, + ); + + if (sent) { + await markAutoProvisionDigestSent(candidate.workos_organization_id); + delivered++; + logger.info( + { + orgId: candidate.workos_organization_id, + memberCount: members.length, + adminEmailCount: adminEmails.length, + }, + 'Auto-provision digest delivered', + ); + } else { + // Slack delivery failed (no admins on Slack, channel issue, etc.). Don't + // mark watermark so the next run retries; future Slack-mapped admins + // will pick it up. + skipped++; + logger.info( + { orgId: candidate.workos_organization_id, memberCount: members.length }, + 'Auto-provision digest: slack delivery skipped', + ); + } + } catch (err) { + failed++; + logger.warn( + { err, orgId: candidate.workos_organization_id }, + 'Auto-provision digest failed for org', + ); + } + } + + if (candidates.length > 0) { + logger.info( + { candidateOrgs: candidates.length, delivered, skipped, failed }, + 'Auto-provision digest pass complete', + ); + } + + return { candidateOrgs: candidates.length, delivered, skipped, failed }; +} + +function buildSlackMessage( + orgName: string, + orgId: string, + members: NewAutoProvisionedMember[], +): { text: string; blocks: any[] } { + const teamUrl = `${APP_URL}/team?org=${orgId}`; + const memberLines = members.map(m => { + const displayName = [m.first_name, m.last_name].filter(Boolean).join(' ').trim(); + const namePart = displayName ? `${escapeSlackMrkdwn(displayName)} (${escapeSlackMrkdwn(m.email)})` : escapeSlackMrkdwn(m.email); + const date = m.joined_at instanceof Date + ? m.joined_at.toISOString().slice(0, 10) + : String(m.joined_at).slice(0, 10); + return `• ${namePart} — joined ${date}`; + }); + + const summary = members.length === 1 + ? `1 person joined *${escapeSlackMrkdwn(orgName)}* via verified-domain auto-add since the last digest.` + : `${members.length} people joined *${escapeSlackMrkdwn(orgName)}* via verified-domain auto-add since the last digest.`; + + return { + text: `${members.length} new auto-joined member${members.length === 1 ? '' : 's'} in ${orgName}`, + blocks: [ + { + type: 'section', + text: { + type: 'mrkdwn', + text: `${summary}\n\n${memberLines.join('\n')}`, + }, + }, + { + type: 'section', + text: { + type: 'mrkdwn', + text: `<${teamUrl}|Review or change roles> · turn auto-add off in the *Verified Domains* card on the same page if you'd rather invite explicitly.`, + }, + }, + ], + }; +} diff --git a/server/tests/integration/membership-webhook.test.ts b/server/tests/integration/membership-webhook.test.ts index 781291793f..606c8ee607 100644 --- a/server/tests/integration/membership-webhook.test.ts +++ b/server/tests/integration/membership-webhook.test.ts @@ -16,6 +16,9 @@ import { findSuccessorForPromotion, setMembershipRole, autoLinkByVerifiedDomain, + findOrgsWithNewAutoProvisionedMembers, + listNewAutoProvisionedMembers, + markAutoProvisionDigestSent, } from '../../src/db/membership-db.js'; import type { WorkOS } from '@workos-inc/node'; import type { Pool } from 'pg'; @@ -605,6 +608,143 @@ describe('Membership webhook DB operations', () => { }); }); + describe('Auto-provision digest queries', () => { + const DIGEST_ORG_ID = 'org_digest_test'; + const DIGEST_USER_NEW = 'user_digest_new'; + const DIGEST_USER_OLD = 'user_digest_old'; + const DIGEST_USER_INVITED = 'user_digest_invited'; + + beforeEach(async () => { + await pool.query('DELETE FROM organization_memberships WHERE workos_organization_id = $1', [DIGEST_ORG_ID]); + await pool.query('DELETE FROM organizations WHERE workos_organization_id = $1', [DIGEST_ORG_ID]); + await pool.query( + `INSERT INTO organizations (workos_organization_id, name, is_personal, subscription_status, auto_provision_verified_domain, last_auto_provision_digest_sent_at, created_at, updated_at) + VALUES ($1, 'Digest Test Org', false, 'active', true, $2, NOW(), NOW())`, + [DIGEST_ORG_ID, new Date('2026-04-01T00:00:00Z')], + ); + }); + + afterAll(async () => { + await pool.query('DELETE FROM organization_memberships WHERE workos_organization_id = $1', [DIGEST_ORG_ID]); + await pool.query('DELETE FROM organizations WHERE workos_organization_id = $1', [DIGEST_ORG_ID]); + }); + + async function seedMember(opts: { + userId: string; + email: string; + source: 'verified_domain' | 'invited' | 'admin_added'; + createdAt: Date; + }) { + await pool.query( + `INSERT INTO organization_memberships + (workos_user_id, workos_organization_id, email, role, seat_type, provisioning_source, created_at, updated_at, synced_at) + VALUES ($1, $2, $3, 'member', 'community_only', $4, $5, $5, $5)`, + [opts.userId, DIGEST_ORG_ID, opts.email, opts.source, opts.createdAt], + ); + } + + it('finds orgs with new verified_domain members since the watermark', async () => { + // Member joined BEFORE watermark — excluded. + await seedMember({ + userId: DIGEST_USER_OLD, + email: 'old@digest.com', + source: 'verified_domain', + createdAt: new Date('2026-03-15T00:00:00Z'), + }); + // Member joined AFTER watermark — included. + await seedMember({ + userId: DIGEST_USER_NEW, + email: 'new@digest.com', + source: 'verified_domain', + createdAt: new Date('2026-04-15T00:00:00Z'), + }); + // Invited member — wrong source, excluded. + await seedMember({ + userId: DIGEST_USER_INVITED, + email: 'invited@digest.com', + source: 'invited', + createdAt: new Date('2026-04-15T00:00:00Z'), + }); + + const rows = await findOrgsWithNewAutoProvisionedMembers(); + const target = rows.find(r => r.workos_organization_id === DIGEST_ORG_ID); + expect(target).toBeDefined(); + expect(target!.new_member_count).toBe(1); + expect(target!.org_name).toBe('Digest Test Org'); + }); + + it('skips orgs with auto_provision_verified_domain disabled', async () => { + await pool.query( + 'UPDATE organizations SET auto_provision_verified_domain = false WHERE workos_organization_id = $1', + [DIGEST_ORG_ID], + ); + await seedMember({ + userId: DIGEST_USER_NEW, + email: 'new@digest.com', + source: 'verified_domain', + createdAt: new Date('2026-04-15T00:00:00Z'), + }); + + const rows = await findOrgsWithNewAutoProvisionedMembers(); + expect(rows.find(r => r.workos_organization_id === DIGEST_ORG_ID)).toBeUndefined(); + }); + + it('skips orgs with no new members since watermark', async () => { + await seedMember({ + userId: DIGEST_USER_OLD, + email: 'old@digest.com', + source: 'verified_domain', + createdAt: new Date('2026-03-15T00:00:00Z'), // before watermark + }); + + const rows = await findOrgsWithNewAutoProvisionedMembers(); + expect(rows.find(r => r.workos_organization_id === DIGEST_ORG_ID)).toBeUndefined(); + }); + + it('treats NULL watermark as the beginning of time', async () => { + await pool.query( + 'UPDATE organizations SET last_auto_provision_digest_sent_at = NULL WHERE workos_organization_id = $1', + [DIGEST_ORG_ID], + ); + await seedMember({ + userId: DIGEST_USER_NEW, + email: 'new@digest.com', + source: 'verified_domain', + createdAt: new Date('2026-01-01T00:00:00Z'), + }); + + const rows = await findOrgsWithNewAutoProvisionedMembers(); + const target = rows.find(r => r.workos_organization_id === DIGEST_ORG_ID); + expect(target).toBeDefined(); + expect(target!.new_member_count).toBe(1); + expect(target!.last_sent_at).toBeNull(); + }); + + it('lists members chronologically, excluding non-verified-domain sources', async () => { + const t1 = new Date('2026-04-10T00:00:00Z'); + const t2 = new Date('2026-04-12T00:00:00Z'); + const t3 = new Date('2026-04-14T00:00:00Z'); + + await seedMember({ userId: 'u_b', email: 'b@digest.com', source: 'verified_domain', createdAt: t2 }); + await seedMember({ userId: 'u_a', email: 'a@digest.com', source: 'verified_domain', createdAt: t1 }); + await seedMember({ userId: 'u_c', email: 'c@digest.com', source: 'verified_domain', createdAt: t3 }); + await seedMember({ userId: 'u_inv', email: 'inv@digest.com', source: 'invited', createdAt: t2 }); + + const members = await listNewAutoProvisionedMembers(DIGEST_ORG_ID, new Date('2026-04-01T00:00:00Z')); + expect(members.map(m => m.email)).toEqual(['a@digest.com', 'b@digest.com', 'c@digest.com']); + }); + + it('markAutoProvisionDigestSent updates the watermark', async () => { + const sent = new Date('2026-04-26T12:00:00Z'); + await markAutoProvisionDigestSent(DIGEST_ORG_ID, sent); + const row = await pool.query<{ last_auto_provision_digest_sent_at: Date }>( + 'SELECT last_auto_provision_digest_sent_at FROM organizations WHERE workos_organization_id = $1', + [DIGEST_ORG_ID], + ); + expect(row.rows[0].last_auto_provision_digest_sent_at.toISOString()).toBe(sent.toISOString()); + }); + }); + describe('upsertOrganizationMembership provisioning_source', () => { it('writes provisioning_source on insert', async () => { await upsertOrganizationMembership({