Releases: hypercerts-org/certified-group-service
Release list
v0.5.0
Who should read this release
Minor Changes
-
#61
4a73f38Thanks @aspiers! - Operators can reassign a group's owner through a new admin API.Affects: Operators
This adds an operator-only admin endpoint. It is deliberately not part of the member-facing API — it is authenticated with the service admin password, not a member's JWT or API key — so client app developers building on the group service have nothing to call or adapt; only operators running an instance are affected.
Operators: a new optional env var
CGS_ADMIN_PASSWORDenables operator-only admin endpoints (app.certified.group.admin.*), authenticated with HTTP Basic auth rather than group membership — the same mechanism the upstream AT Protocol reference PDS uses forcom.atproto.admin.*. When unset, all admin endpoints are disabled. The trust model is the one a PDS operator already operates under, applied to the groups CGS hosts; admin actions are audit-logged. The first such endpoint,app.certified.group.admin.setOwner, reassigns a group's owner (including installing an owner who is not yet a member, for operator recovery when the incumbent is unavailable). Seedocs/deployment.md#admin-endpointsand the API reference for configuration and the endpoint contract.
v0.4.0
Who should read this release
- Client app developers:
Minor Changes
v0.3.0
Who should read this release
- Client app developers:
- Operators:
Minor Changes
-
#34
e6c8f87Thanks @aspiers! - Backend services can now use revocable API keys for group-owned records.Affects: Client app developers, Operators
Client app developers: group owners can create, list, and revoke keys with
app.certified.group.keys.create,app.certified.group.keys.list, andapp.certified.group.keys.delete. Key auth usesX-API-Keyand supports explicit scopes plus reusable permission-set scopes such asinclude:org.hypercerts.authWrite, so apps no longer need to list every record permission by hand. Seedocs/design/api-keys.md,docs/design/api-key-permission-sets.md, anddocs/integration-guide.mdfor request shapes, supported permissions, and examples.Operators: no new environment variables or manual migration are required. Leaked keys are recognizable by the
cgsk_prefix and can be revoked by the group owner.
Patch Changes
-
#43
a059e0fThanks @aspiers! - Authentication failures are now logged server-side, so an operator can diagnose a rejected request from the logs instead of having to reproduce it.Affects: Operators
Operators: every auth rejection in the verifier now emits a
warn-level log line"Auth verification failed"before the request is refused. Previously the fallback error handler returned the error to the client but logged nothing, so a production401(e.g.Invalid audience) left no server-side trace of who called or which group they targeted.- JWT (service-auth) failures mostly log
{ reason, nsid, jwt: { header, payload } }. The JWT is decoded for logging without verifying its signature, and the signature segment is dropped — it is a bearer credential and is never written to the logs (jwtisnullfor a token that is not a well-formed three-part base64url JWT). The exception isMissing auth token, which fires before any token exists and logs only{ reason, path }.reasonis one of:Missing auth token,verifyJwt threw,Token lifetime check failed,jwt audience does not match service did,repo did not resolve to a known group,Invalid audience,Missing jti,Replayed token. - API-key failures log
{ reason, authKind: 'apiKey', keyRef, groupDid }— only the non-secret key reference, never the rawX-API-Keyvalue.reasonis one of:Malformed API key,Missing repo for API-key request,repo did not resolve to a known group,Invalid API key,Corrupt API-key scopes. - No request that previously succeeded is affected, and the HTTP responses returned to clients are unchanged — this is purely additional logging at the existing
logLevel.
- JWT (service-auth) failures mostly log
v0.2.1
Patch Changes
-
Republish the container image. The v0.2.0 image build failed (its version-stamping step had no
.cgs-versionto read), so noghcr.io/hypercerts-org/group-servicetags were published for v0.2.0. The publish workflow now stamps the version before building; v0.2.1 is the first release to produce a working image. No functional change to the service versus what v0.2.0 would have shipped.Affects: Operators
v0.2.0
Who should read this release
- End users:
- Client app developers:
- Apps now name the target group with an explicit
repofield instead of overloading the service-auth token's audience. - A group's owner can now remove the group from the service.
- You can now turn an existing account into a group, instead of always creating a brand-new one.
- The health check now reports the running service version, and the same check is also reachable at
/xrpc/_health.
- Apps now name the target group with an explicit
- Operators:
- Apps now name the target group with an explicit
repofield instead of overloading the service-auth token's audience. - A group's owner can now remove the group from the service.
- You can now turn an existing account into a group, instead of always creating a brand-new one.
- The health check now reports the running service version, and the same check is also reachable at
/xrpc/_health.
- Apps now name the target group with an explicit
Minor Changes
-
#33
677298eThanks @aspiers! - Apps now name the target group with an explicitrepofield instead of overloading the service-auth token's audience.Affects: Client app developers, Operators
Client app developers: group-scoped methods take a
repofield (a handle or DID) naming the target group, with the JWTaudset to the service DID — the shape a stock@atproto/apiclient already emits. The old form (group inaud, norepo) still works but is deprecated.Legacy (deprecated) New (supported) Group named by JWT audexplicit repoJWT audthe group DID the service DID repofieldabsent present Deprecation header Deprecation: true+Linknone - Behaviour change to adapt to now:
repois the group selector, not a cross-check — the old403"repo field must match the group DID" is gone; areponaming no registered group returns401 Unknown group. RBAC is unchanged: you can only target groups you have a role in. - How to migrate (per-method
repoplacement, the coupledrepo+audswitch, non-proxied vs proxied calls, detecting un-migrated calls): seedocs/aud-migration.md. Design rationale and security analysis:docs/design/aud-deprecation.md.
Operators: no new environment variables and no migration;
SERVICE_DIDis unchanged. The service now serves itsdid:webdocument atGET /.well-known/did.json(a public, unauthenticated route, sibling to/health); it must be publicly reachable, since that is how the service DID resolves and how service proxying targets the service. A rate-limitedwarnlog flags any client still on the legacy form — one line per caller per 15 minutes. - Behaviour change to adapt to now:
-
#30
e4a0aacThanks @aspiers! - A group's owner can now remove the group from the service.Affects: End users, Client app developers, Operators
End users: if you are a group's owner, you can now delete the group from the service. This removes the group and its membership from the service only — the underlying account and its data are left untouched, so the account continues to exist and could be added back later. Only the owner can do this; admins and members cannot.
Client app developers: a new procedure
app.certified.group.destroyremoves a group from the service, gated on theownerrole (new RBAC operationgroup.destroy). It removes only the service's record of the group; the underlying PDS account is left intact, so the account can be re-imported afterwards viaapp.certified.group.import. Call it group-scoped, the same auth style as the other per-group methods. Full request/response and errors are indocs/api-reference.mdanddocs/integration-guide.md.Heads-up: like the other per-group methods,
destroycurrently names the target group via the JWTaud. That overload is being deprecated (issue #27) in favour of an explicit request-level group field, withaudreverting to the group service's own DID. Theaud= group DID form will be supported during a transition window and then removed — build against the explicit-group form once it ships.Operators:
destroyis served on/xrpc/app.certified.group.destroy; no new environment variables. It deletes the group'sgroupsrow andmember_indexentries (in one transaction) and then the per-group SQLite file underdata/groups/. The destroy is recorded in the service log, not in the (deleted) per-group audit log. -
#30
f265075Thanks @aspiers! - You can now turn an existing account into a group, instead of always creating a brand-new one.Affects: Client app developers, Operators
Client app developers: a new procedure
app.certified.group.importis the sibling ofapp.certified.group.register— it reuses an existing account instead of creating one. You supply the account's app password so the service can act on its behalf, and the JWT must be signed by the account being imported (iss= the account's DID), not by the prospective owner. Two consequences worth knowing: the service holds no recovery key for an imported account (the owner's own credentials are their credible exit), andimportdoes not modify the account's DID document. Full request/response, auth model, and errors are indocs/api-reference.md(Group lifecycle),docs/integration-guide.md(Step 1b), anddocs/design/group-import.md.Operators:
importis served on/xrpc/app.certified.group.import(service-auth, likeregister); no new environment variables. Imported groups are stored in thegroupstable withencrypted_recovery_keyleftNULL, distinguishing them from registered groups, and are driven via the per-grouppds_urlresolved at import time — which may differ fromGROUP_PDS_URL. -
#35
a906bd8Thanks @aspiers! - The health check now reports the running service version, and the same check is also reachable at/xrpc/_health.Affects: Client app developers, Operators
Client app developers:
GET /healthresponse gains two fields:service(always"group-service") andversion(e.g."0.1.0+90d10b96"). The existingstatus: "ok"/503 { status: "error", message: "database unreachable" }behaviour is unchanged, so existing health checks keep working.- A new
GET /xrpc/_healthroute returns the identical body to/health(including the same 503-on-DB-failure semantics). This mirrors the upstream PDS convention of exposing/xrpc/_health; the group service has no upstream PDS, so it serves the route itself. Note it returns the full{ status, service, version }object, not the bare{ version }some atproto services return.
Operators:
- The reported version resolves in this order: the
CGS_VERSIONenv var, then a.cgs-versionfile written at image-build time, then theversionfield inpackage.json. SetCGS_VERSIONto override the stamp (e.g.CGS_VERSION=0.1.0+abcdef01). - On Railway, the Docker build stamps
<package.json version>+<short commit sha>automatically fromRAILWAY_GIT_COMMIT_SHA— no action needed. - For local
docker build, run./scripts/stamp-version.shfirst to write.cgs-version; the build fails withERROR: .cgs-version not foundotherwise..cgs-versionis gitignored.