Repository navigation
Conversation
Contributor
|
The |
obany
reviewed
Oct 20, 2026
This was referenced Oct 23, 2026
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.
📝 Description
Let an account exist before it has a DID, then attach a self-controlled DID after sign-in. The account id is allocated at create and stays the token subject. This is the api-auth half of trade-data issue 7, on
feature/did-onboarding, targetingnext.ContextIdKeys.Accountcarries the account id.ContextIdKeys.Useris the DID, and only once one is stored. Password change and adoption read the account key and setrequiresAuthorization: false. They stay authenticated. Logout records the account id, and its route still requires a DID. Adoption callsidentityCreate(undefined, undefined, { selfControlled: true })on the in-process identity service (@3sixty/identity-models0.20.0-next.3), not the identity HTTP route. A failed save removes the document and its signing key. No organisation credential is issued. Node does not register the account key. On IOTA, self-controlled create needs a gas station. Entity storage does not.Stored scope stays removed, the password-policy route stays, and the escalated-privilege guard stays. When a create supplies a DID, that guard runs before the identity-uniqueness check.
🔄 Type of Change
🛠️ Changes Made
userIdwithRandomHelper.generateUuidV7()after the email, password, and organisation checks. The request has no account id field. A supplieduserIdentityis trimmed and stored. A blank value is rejected before a row is written. Omitting it leavesidentityempty.organizationIdentityfalls back to the request organisation, and a missing organisation is rejected before anything is written. A DID that is already stored is rejected. Two accounts with no DID each receive their ownuserId.userIdis required.userIdentityis optional and is returned only when a DID is stored. Update may replace an explicit DID and leavesuserIdunchanged.userId. That id is the tokensub. The email is the login name.IAuthTokenContext.userIdentitystays the account id.ContextIdKeys.Accountwhenever the token verifies, and setsContextIdKeys.Useronly when the stored identity starts withdid:. No DID leavesUserunset. A value that is not a DID is not written there.ContextIdKeys.Account. Logout records that key as the audit actor. The escalated-privilege guard still readsContextIdKeys.User, so a caller with no DID is not treated as escalated.PUT /authentication/passwordandPOST /authentication/identitysetrequiresAuthorization: falseand stay authenticated. Every other route still requires the DID.AuthenticationUseris entity version 2:userIdrequired,identityoptional. Version 1 remains the authorization shape, with a required identity and no account id. The version 1 to 2 migration allocatesuserIdwithRandomHelper.generateUuidV7()and keepsidentity. A version 0 row drops its stored scope on the way through. Tokens whosesubis an email or a DID no longer verify.POST /authentication/identityreturns 204 and leaves the session cookie in place for a bearer caller and a cookie caller. The identity component type staysidentity-serviceuntil the engine passesidentityComponentType. The localIIdentityAdoptionslice is gone. A missing account returns not found. An identity that already starts withdid:conflicts and leaves the row unchanged. Otherwise the new document id is saved and an account-updated audit entry records the account id as the actor. A later refresh keeps the samesub.identity-adopt, 5 attempts per 15 minutes, checked before a document is created. The key is the account id. If the save fails, the new DID is removed withremoveKeys,removeDocumentKey, and itself as the controller.@3sixty/identity-modelsis a new dependency, atnext(0.20.0-next.3). Resolvingnextfor this package also moves@3sixty/contextto0.20.0-next.4.GET /authentication/policystays on the same route group.apirepository is unchanged. A node does not pick this up until api-auth publishes it.🧪 Testing
Specs cover create with and without a DID, a blank identity, a missing organisation, two accounts that each receive an id, and escalated-privilege checks. Header specs cover a DID on both context keys, an account with no DID on the account key only, and a stale
Uservalue cleared when there is no DID. Password change and adoption load the account whenUseris absent. Adoption specs cover a stored DID, a DID that is already stored, a failed identity create, a failed save that removes the document key, the audit entry, the rate check, and a refresh that keeps the same account id. The password and adoption routes assertrequiresAuthorization: falseand do not skip authentication. Schema specs migrate a version 1 row and a version 0 row, and a login spec signs in with the id the migration allocated. The entity-storage service package builds, and its 150 tests pass. Repositorypnpm run qualityhas not been re-run since this change.