Repository navigation
Conversation
The Dewee identity server SDK is not on npm yet. Pack 0.1.0-beta.0 from the dewee repository (packages/dewee-identity at nextlevelbuilder/dewee dev) into vendor/ and use it as a file: dev dependency for the tests. For users it is an optional peer dependency, needed only when the handler signs users in, so the published package never points at a local file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… exchange
A host passes identity.authenticate(req). On /session the handler exchanges
the signed-in user for a Dewee identity session (one agent, chat only,
short-lived) and embeds it in the visitor token (ids, ida), with exp capped at
the session expiry. /chat and /history then connect with identity_session and
no user_id, so the gateway derives the user; a token stays bound to its user
whatever cookie a later request carries.
Fail closed: an exchange failure issues no session, a null or throwing
authenticate falls back to anonymous only when identity is not required, and
identity cannot be combined with client credentials. Gateway identity errors
are forwarded as {error:{code, remediation}} (JSON before streaming, SSE
during a run); the widget shows localized copy and re-requests /session after
LINK_EXPIRED or LINK_REVOKED. POST /link starts a web-first link and the
widget shows the proof code with the chat accounts to send it to.
Anonymous mode is unchanged when the identity option is absent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
README "Signed-in users" (setup, options, fail-closed rules, linking) and the HTTP contract for the identity additions to /config, /session, /chat, /history, the typed identity errors and POST /link. The PHP backend does not support signed-in users yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gned-in sessions - Refuse a connect whose answer does not name a tuc: user, so a gateway that ignored identity_session never runs the turn as the operator token's user (503 SOURCE_UNAVAILABLE). - Move the @dewee/identity calls to a separate dewee-webchat/server/identity entry passed as identity.sdk; dewee-webchat/server no longer references the SDK, so anonymous-only hosts bundle it with webpack without the SDK. - Seal the identity session in the visitor token with AES-256-GCM (HKDF key from the handler secret, bound to visitor and agent). - Reuse one identity session per user until it nears expiry, drop it when the gateway reports it dead, and measure its lifetime from receipt; return expiresIn so the widget refreshes min(60s, lifetime/2) before the end. - Rate-limit signed-in sessions per user and links in their own bucket; hand a valid anonymous token back without minting. - Add opt-in rebindEachRequest: chat, history and link re-run authenticate and compare a sealed subject digest. - A throwing authenticate is 503, exchange refusals for operators are 500 identity_misconfigured, link conflicts get their own codes, the bound agent is checked against the current allowlist, and scopes is removed from AuthenticatedUser. - The widget renews a refused signed-in session once for link and history. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e identity SDK Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mits Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed claim size
- Compare the rebinding subject digest with a constant-time byte compare.
- Cap host authenticate calls per IP (new preauth bucket, default 120 per
minute) before authenticate runs on /session and on rebinding; document
that authenticate should be cheap or cached.
- Size the sealed identity claim cap from the real plaintext layout
({"t":...,"h":...} with worst-case JSON escaping and the subject digest),
so a 512-character session token always seals and opens.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
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.
Part of nextlevelbuilder/dewee#1450 (Trusted User Context + Dewee Connect). A host app passes one server-side callback, and the widget then chats as the verified user, with private tools available through Dewee. The host no longer mints tokens, stores per-visitor credentials or exposes the gateway token. Anonymous mode is unchanged when the new
identityoption is absent.What changed
identityhandler option (packages/dewee-webchat/src/server/handler.ts). It takes:sdk:deweeIdentitySdkfrom the newdewee-webchat/server/identityentry;authenticate(req),appId,issuer, an optionalaudiencecheck;assertionSecretorprivateKeyJwk;agent,required,rebindEachRequest,sessionTtlSecondsandallowInsecureHttp.Options are validated at startup.
Separate SDK entry.
dewee-webchat/servernever imports@dewee/identity, not even lazily, so hosts that serve only anonymous visitors bundle it with webpack or esbuild without the SDK. The only module that imports the SDK isdewee-webchat/server/identity.POST /sessioncallsauthenticate. For a signed-in user it exchanges the user for a Dewee identity session, or reuses the one it holds for that user, and wraps it in the visitor token.idsclaim is sealed with AES-256-GCM. The key is derived from the handler secret with HKDF and a distinct info label, and the associated data binds the ciphertext to the visitor and the agent. The browser holds only an opaque value.expis the earlier of the session lifetime, measured from receipt, and the visitor expiry.expiresInis returned as well./chatand/history, when the token hasids:identity_sessionand nouser_id.tuc:user. Otherwise it closes the connection and answers 503SOURCE_UNAVAILABLE, with nothing run.agents.Opt-in
rebindEachRequest./chat,/historyand/linkcallauthenticate(req)again and compare the result with an HMAC digest of the subject sealed in the token; the raw subject is never stored. Another user or no user gets 401IDENTITY_REQUIRED, and the widget re-requests/session. A throwingauthenticategets 503SOURCE_UNAVAILABLE. This is recommended for cookie logins, because a replayed widget token becomes useless under another user's cookie.Typed errors. The seven identity codes are forwarded as
{error:{message, code, remediation}}, or as an SSEerrorevent mid-run, and the widget shows localized en and vi copy.identity_misconfiguredwithout detail.idempotency_key_reused, 409link_in_progressand 503linking_unavailable, each with localized copy.Web-first linking.
POST {endpoint}/linkcallsstartLinkand returns{proofCode, expiresAt, deepLinks}.chat.startLink()shows the code. Nobody types a sender id.Widget sessions.
min(60s, lifetime/2), with the lifetime taken from the server'sexpiresInand counted from receipt.link()andhistory()drop a refused signed-in session and renew it once, the same waychat()does.Rate limits, the in-memory defaults:
/session: 30 per user per 10 minutes, applied afterauthenticate;/link: its own bucket, 5 per user per 10 minutes;One identity session per user and agent is cached, bounded at 1000 entries, until it nears expiry. A session the gateway reports dead is dropped.
RateLimiterbuckets gainverifiedandlink.Fail closed.
authenticategets 503 whether or notrequiredis set.authenticategets an anonymous session only whenrequiredis not set.scopesremoved fromAuthenticatedUser. The docs point to the SDKlifecycle({event: "scopes_changed"}).Docs. "Signed-in users" sections in both READMEs, and the identity sections in
docs/http-contract.md. They now cover:authenticate.Review fixes in this update (84d2ba1, 8421b88, 1589398)
identity_sessionuser_idmust start withtuc:. Otherwise the connection is closed and the request gets 503SOURCE_UNAVAILABLE. A fake-gateway test ignores the session and asserts that nothing ran.@dewee/identitydewee-webchat/server/identityentry passed asidentity.sdk.scripts/bundler-smoke.mjs(CI steppnpm test:bundlers) bundles the anonymous server with webpack and esbuild with the SDK made unresolvable and runs the bundles. Against the previous build it reproduced the failure (Module not found: Can't resolve '@dewee/identity'atdist/server.js:375)./sessionand/linkshare the per-IP limiterverifiedbucket afterauthenticate, valid anonymous tokens reused, separatelinkbucket, limits documentedmin(60s, ttl/2), measured from receipt usingexpiresIn; the server cache uses the same marginidssealed with AES-256-GCM (HKDF from the handler secret); docs describe the token's exact powers/sessionscopessilently ignoredAuthenticatedUser; the docs point tolifecyclescopes_changedend_userremediation andNOT_FOUNDmap to 500identity_misconfigured; the contract table matchesauthenticateSOURCE_UNAVAILABLEalways; README fixedlink()and revoked sessionslink()(andhistory()) drop and renew once; tests cover the SSE renew path and the link renewIDEMPOTENCY_KEY_REUSEDandFAILED_PRECONDITIONmap to 409 with their own codes;NOT_FOUNDmaps to 503linking_unavailableagentson every requestidentity.rebindEachRequestwith a sealed subject digest; tests belowLinking code host apps no longer need
What a host that knows its user (Campus) had to build before this PR, and what replaces it:
identity.authenticate(req) => { subject }; the handler does the exchangeidentity.rebindEachRequest: trueuser_idfor "logged-in" visitorsuser_idwithids, so the client cannot choose the userchat.startLink()shows a Dewee proof code; Dewee checks both sidesIsolation test
packages/dewee-webchat/test/identity.test.tshas 34 tests. They run against a fake gateway that serves the identity HTTP routes (verifying HS256 and ES256 assertions) and the WS v3 protocol on one port. They cover:Isolation
{token, identity_session}, with nouser_ideven when the client sends one.agents.Rebinding
IDENTITY_REQUIREDon chat, history and link.authenticategets 503.Sealed token
Fail closed
sdk.authenticategets 503 whether or notrequiredis set.required, no session is issued.identity_misconfigured.identity_sessiongets 503, and nothing is sent.Reuse and limits
LINK_EXPIRED.Typed errors and linking
LINK_EXPIREDcomes back as 401 for chat and history, and a mid-runLINK_REVOKEDarrives as an SSE event./linkreturns the proof code, refuses anonymous visitors, and maps reused, in-progress and switched-off.Widget
LINK_REVOKED.expiresInbeats a skewedexpiresAt.Secret hygiene
dand every raw identity session issued in the file are absent from all browser-facing bodies and headers.Test results (Windows, Node 24.11, pnpm 10.18), head 1589398
php -l php/dewee-webchat.phpruns in CI because PHP is not installed locally.php/**is untouched.Vendored SDK tarball (temporary)
@dewee/identityis not published to npm yet.vendor/dewee-identity-0.1.0-beta.0.tgzwas packed withpnpm --filter @dewee/identity packfrom nextlevelbuilder/deweedev; the SDK is unchanged throughc6e1fc3b8. It is afile:dev dependency for the tests. For users,@dewee/identityis an optional peer dependency used only bydewee-webchat/server/identity. Once the SDK is published, the dev dependency switches to npm andvendor/is deleted. See the licensing gate above: the tarball is CC-BY-NC-4.0 inside an MIT repository.Notes for review
audienceis accepted only as a consistency check (dewee:<appId>), because the SDK derives the audience from the application id.identityset,/configturns the agent picker off and reports the bound agent as the default, because a Dewee session is bound to one agent.@dewee/identityreturns only an absoluteexpiresAt(the exchange answer has noexpires_in). The handler therefore measures the lifetime on receipt, trusts it only while it fits the requested TTL (otherwise it falls back to the requested or default TTL and logs clock skew), and sends that to the widget asexpiresIn.authenticateruns before any limiter on/session. That is needed to key the per-user bucket, so the cost of the host's own login check is the host's concern.identity_sessioncase is being done separately in dewee.PHP parity
Surface parity:
php/dewee-webchat.phpis N/A in this PR because the identity exchange needs JWS signing and AES-GCM sealing, which the PHP adapter does not have yet. The follow-up is #5.Refs nextlevelbuilder/dewee#1450 · PHP follow-up: #5
🤖 Generated with Claude Code