Skip to content

feat: core identity system and push notifications#224

Merged
0xmrpeter merged 17 commits intodevelopfrom
feature/identity-and-notifications
Apr 14, 2026
Merged

feat: core identity system and push notifications#224
0xmrpeter merged 17 commits intodevelopfrom
feature/identity-and-notifications

Conversation

@0xmrpeter
Copy link
Copy Markdown
Contributor

Summary

  • Core Identity System (@openacp/identity built-in plugin): User + Identity model with cross-platform linking, role-based access control, auto-registration on first message, REST API, and App user setup flow via /identity/setup + link codes
  • Push Notification Service: Replaces NotificationManager with NotificationService supporting user-targeted delivery via ctx.notify(), cross-platform routing through identity system, and SSE user-level connections for App notifications without active sessions
  • Core type extensions: 7 identity lifecycle events on EventBus, 4 new plugin permissions (identity:read/write, identity:register-source, notifications:send), sendUserNotification?() on IChannelAdapter, createdBy/participants on SessionRecord

Key design decisions

  • User + Identity separation: UserRecord (person) and IdentityRecord (platform account) — one person can have multiple identities across Telegram, Discord, App
  • Canonical vs platform usernames: Agent sees @lucas (canonical), adapters rewrite to platform-native mentions (@lucas_tg on Telegram, <@456> on Discord)
  • Identity setup flow: Exchange unchanged, new POST /identity/setup after exchange for API/App users. Link codes for multi-device
  • Backward compatible: StoredToken.userId? optional, NotificationManager alias exported, sendUserNotification?() optional on adapters

Specs

  • docs/superpowers/specs/2026-04-12-core-identity-system-design.md
  • docs/superpowers/specs/2026-04-12-core-push-notification-design.md
  • docs/superpowers/plans/2026-04-12-core-identity-and-notifications.md

Test plan

  • 103 new tests pass (identity-service, kv-store, auto-register, notification)
  • Existing test suite: no regressions (6 pre-existing failures unchanged)
  • Build clean (pnpm build)
  • Manual: send message from Telegram → user auto-registered in identity store
  • Manual: openacp remote → exchange → /identity/setup/auth/me shows userId
  • Manual: GET /api/v1/sse/events with JWT → receives heartbeats

Known gaps (v2)

  • identity:seen event not emitted (needs session context)
  • SessionRecord.createdBy/participants defined but not populated
  • REST route input validation (Zod schemas) not added

🤖 Generated with Claude Code

0xmrpeter and others added 17 commits April 12, 2026 00:52
Defines all types for the identity system: IdentityId branded type with
format/parse helpers, UserRecord, IdentityRecord, SessionInfo, UserRole,
and the full IdentityService interface with JSDoc on all public APIs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…mentation

Defines the persistence contract (IdentityStore) and its PluginStorage-backed
implementation (KvIdentityStore). Uses flat kv.json keys under users/,
identities/, and idx/ prefixes. Includes 31 tests covering CRUD, indexes,
list filtering, and getUserCount.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements all IdentityService operations: createUserWithIdentity (first
user auto-admin), updateUser (with username index rotation), setRole,
createIdentity, link (merge younger into older user), unlink (split into
new user), plugin data namespacing, and resolveCanonicalMention. Emits
events on all state changes. Includes 45 tests covering all flows, merges,
edge cases, and error paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… SessionFactory userId

- Add 7 identity lifecycle events to EventBusEvents (identity:created/updated/linked/unlinked/userMerged/roleChanged/seen)
- Add userId? to session:created event payload for identity correlation
- Add IDENTITY_* constants to BusEvent object in events.ts
- Add identity:read, identity:write, identity:register-source, notifications:send to PluginPermission
- Add createdBy? and participants? fields to SessionRecord interface
- Fix SessionFactory.create(): pass params.userId to session:beforeCreate middleware (was hardcoded '')
- Add userId? to SessionCreateParams interface
- Forward userId to session:created event emission via createParams.userId

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Creates message:incoming middleware at priority 110 that runs after security
(100) — ensuring blocked users are rejected before identity records are created.

On each incoming message:
- Unknown identity → creates user + identity via IdentityServiceImpl
- Known identity → throttled lastSeenAt update (max once per 5 min) and
  platform field sync if channelUser reports changes
- Injects meta.identity snapshot for downstream hooks to avoid redundant lookups

Also adds 8 tests covering: creation flow, idempotency, admin auto-promotion,
member role for subsequent users, displayName sync, userId fallback, lastSeenAt
throttling, and no-meta guard.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…Task 7)

Creates the identity plugin (src/plugins/identity/index.ts) that wires together
KvIdentityStore, IdentityServiceImpl, and the auto-register middleware. Registers:
- 'identity' service in ServiceRegistry for other plugins to consume
- message:incoming middleware at priority 110
- /whoami command for users to set their display name

Registers the plugin in core-plugins.ts immediately after securityPlugin so
identity boots before file-service, context, and adapter plugins. The comment
documents the boot-order requirement.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds /api/v1/identity route group:
- GET/PUT /users, /users/me, /users/:userId — user CRUD
- PUT /users/:userId/role — admin-only role assignment
- GET /users/:userId/identities, /resolve/:identityId — identity lookups
- POST /link, /unlink — cross-platform identity linking
- GET /search — user search
- POST /setup — first-time identity claim for API token holders
- POST /link-code — one-time code for multi-device account linking

Routes are wired in identity plugin setup() after api-server service is
available. Token-store dependency is resolved lazily via ctx.getService() so
the identity plugin boots cleanly without api-server.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- StoredToken.userId?: string — optional field persisted to tokens.json
  (backward compatible: existing tokens without the field are unaffected)
- TokenStore.setUserId() / getUserId() — called by identity plugin after
  /identity/setup to associate a canonical userId with a JWT token
- TokenStore registered as 'token-store' service so identity plugin can
  resolve it without a direct import dependency
- /auth/me response extended with userId, displayName (fetched lazily from
  identity service if available), and claimed flag — identity service is
  accessed via optional getIdentityService() resolver so auth routes have
  no hard dependency on the identity plugin being loaded

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add optional sendUserNotification() method to IChannelAdapter interface
to enable adapters to send direct user-targeted messages by platform ID.
ChannelAdapter base class provides a no-op default so existing adapters
don't need to implement it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… delivery

Replace NotificationManager with NotificationService that keeps full
backward compatibility (notify/notifyAll) and adds notifyUser() for
delivering cross-platform notifications via the identity system.

NotificationManager is exported as an alias so all existing imports
continue to work without changes. The plugin index now wires the identity
resolver on startup and listens for late-loading identity plugin events.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Expose ctx.notify() as a fire-and-forget method on PluginContext for
sending user-targeted notifications. Gated by 'notifications:send'
permission. Delegates to the NotificationService.notifyUser() via
ServiceRegistry — no direct dependency on the notifications plugin.

Also extend the NotificationService interface in types.ts to include
the optional notifyUser() method, making the contract explicit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add userId field to SSEConnection, a userIndex map for O(1) delivery
targeting, addUserConnection() for connections not tied to a session,
pushToUser() with backpressure handling, and clean up userIndex in
removeConnection() and cleanup().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add GET /events route that streams notifications to authenticated users
with identity set up (token-store lookup via getUserId). Inject getUserId
callback through SSERouteDeps from index.ts. Add sendUserNotification()
to SSEAdapter that serializes and pushes notification:text events to all
user-level connections via pushToUser().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix all identity event names/payloads to match EventBus types:
  identity:userCreated → identity:created with {userId,identityId,source,displayName};
  identity:linked payload → {userId,identityId,linkedFrom};
  identity:userMerged payload → {keptUserId,mergedUserId,movedIdentities};
  identity:unlinked payload → {userId,identityId,newUserId};
  add identity:updated emission in updateUser()
- Fix SSE /events route: check connection limit before reply.hijack() so errors
  can still be sent as HTTP responses (503) rather than writing 503 headers
  after 200 already committed
- Add admin-only auth guard to POST /link and POST /unlink routes
- Add UserNotificationContent interface to notification.ts; replace as any casts
  with typed as unknown as NotificationMessage
- Update identity-service tests to assert correct event names and payloads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ctx.notify() was added to PluginContext in the identity/notifications
feature but TestPluginContext was not updated, causing SDK build failure.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@0xmrpeter 0xmrpeter merged commit 946a8ed into develop Apr 14, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant