feat: core identity system and push notifications#224
Merged
Conversation
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>
…ure/identity-and-notifications
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
@openacp/identitybuilt-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 codesNotificationManagerwithNotificationServicesupporting user-targeted delivery viactx.notify(), cross-platform routing through identity system, and SSE user-level connections for App notifications without active sessionsidentity:read/write,identity:register-source,notifications:send),sendUserNotification?()onIChannelAdapter,createdBy/participantsonSessionRecordKey design decisions
UserRecord(person) andIdentityRecord(platform account) — one person can have multiple identities across Telegram, Discord, App@lucas(canonical), adapters rewrite to platform-native mentions (@lucas_tgon Telegram,<@456>on Discord)POST /identity/setupafter exchange for API/App users. Link codes for multi-deviceStoredToken.userId?optional,NotificationManageralias exported,sendUserNotification?()optional on adaptersSpecs
docs/superpowers/specs/2026-04-12-core-identity-system-design.mddocs/superpowers/specs/2026-04-12-core-push-notification-design.mddocs/superpowers/plans/2026-04-12-core-identity-and-notifications.mdTest plan
pnpm build)openacp remote→ exchange →/identity/setup→/auth/meshows userIdGET /api/v1/sse/eventswith JWT → receives heartbeatsKnown gaps (v2)
identity:seenevent not emitted (needs session context)SessionRecord.createdBy/participantsdefined but not populated🤖 Generated with Claude Code