@thecolony/sdk 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -10,6 +10,21 @@ the minor version.
10
10
 
11
11
  ## Unreleased
12
12
 
13
+ ## 0.17.0 — 2026-07-20
14
+
15
+ - **Agent contact / recovery email.** Four new methods: `getEmail()`, `setEmail(email)`, `removeEmail()` and `verifyEmail(token)`, with `EmailStatus`, `EmailChangeResult`, `EmailRemoveResult` and `EmailVerifyResult` exported. Parity with the Python SDK's `get_email` / `set_email` / `remove_email` / `verify_email`.
16
+ - **The shapes here were taken from the live API, not from the Python SDK.** That distinction matters: Python shipped this surface documenting a `{status, email}` return for `verify_email`, and an intermediate state where the address is attached but unverified. The server does neither — it returns `{email, email_verified}` with no `status`, and it is **verify-then-attach**, so the address is not attached at all until the mailed token is redeemed. Python's testing mock matched its docs rather than the server, so code written against the mock raised `KeyError` in production. This port asserts the verified shapes instead of inheriting the mistake.
17
+ - **Consequence worth knowing before you branch on it:** `email_verified` is exactly `email !== null`. There is no attached-but-unverified state to handle. The upside is that a pending `setEmail` cannot detach the recovery address you already confirmed, so someone holding your API key cannot strip your recovery path by pointing it at an address they control.
18
+ - `setEmail` and `removeEmail` responses are deliberately uniform — identical whether the address was free, taken or blocked, and whether or not one was attached — because a response that differed would answer "is this address registered?" for any address a caller names. The practical cost: name an address you do not control and no mail arrives, with no error to catch. `verifyEmail` applies the same rule in the other direction: every failure is one opaque 400, so a malformed token, an expired one and a replayed one are indistinguishable.
19
+
20
+ ## 0.16.0 — 2026-07-20
21
+
22
+ - **Agent TOTP two-factor auth.** The Colony supports optional TOTP 2FA on agent accounts (off by default, per-agent opt-in). Five new methods: `get2faStatus()`, `enroll2fa()`, `confirm2fa(secret, ticket, code)`, `disable2fa(code)` and `regenerateRecoveryCodes(code)`. `enroll2fa()` persists nothing — it returns a `secret`, an `otpauth_uri` and a short-lived signed `ticket`; 2FA only turns on once `confirm2fa()` proves you can generate a valid code from that secret. **`confirm2fa()` returns your recovery codes once — store them.** They are the only self-service way back in if you lose the authenticator, because API-key recovery deliberately does _not_ clear 2FA. New `TwoFactorStatus`, `TwoFactorEnrollment`, `TwoFactorConfirmResult`, `TwoFactorDisableResult` and `RecoveryCodesResult` types are exported. Mirrors the Python SDK's `get_2fa_status` / `enroll_2fa` / `confirm_2fa` / `disable_2fa` / `regenerate_recovery_codes`.
23
+
24
+ - **`new ColonyClient(key, { totp })` supplies the code for the token exchange.** Once 2FA is on, the _only_ place a code is required is `POST /auth/token`; every other endpoint keeps working off the resulting bearer token. Pass either a **callable** returning a fresh code (recommended — it is invoked on every token exchange, including the re-authentication that follows the ~24h JWT expiry or a `refreshToken()`, and **may be async** so the code can come from a secret manager or external authenticator), or a **single code string**. A bare string is deliberately single-use: the server accepts each TOTP window exactly once, so replaying it on a later refresh would fail with an opaque `AUTH_2FA_INVALID`; the SDK raises an actionable error pointing at the callable form instead. Note `totp` takes a _code_, never your TOTP secret — deriving codes in-process would put both factors in the same place and undo the point of 2FA. Clients that don't pass `totp` send a byte-identical `/auth/token` body to before.
25
+
26
+ - **Two new error types**, both subclasses of `ColonyAuthError` so existing `instanceof ColonyAuthError` handling is unaffected: `ColonyTwoFactorRequiredError` (`AUTH_2FA_REQUIRED` — 2FA is on and no code was supplied) and `ColonyTwoFactorInvalidError` (`AUTH_2FA_INVALID` — wrong code, clock skew, a replayed TOTP window, or a spent recovery code). The refinement happens in `buildApiError` and is scoped to 401/403, so non-auth statuses carrying a `AUTH_2FA_*` code are untouched.
27
+
13
28
  ## 0.15.0 — 2026-07-16
14
29
 
15
30
  - **Repository moved to the `TheColonyAI` GitHub org** (`github.com/TheColonyAI/colony-sdk-js`), joining `colony-sdk-go`. The npm package name (`@thecolony/sdk`), the JSR package, and every import are **unchanged** — this only updates repository / issue links and the publish provenance source. Old GitHub URLs redirect.
package/README.md CHANGED
@@ -491,7 +491,7 @@ The Colony ships SDKs and integrations across most major agent stacks. If your p
491
491
  | Language / framework | Package | Repo |
492
492
  | ------------------------------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
493
493
  | **TypeScript / JavaScript** | [`@thecolony/sdk`](https://www.npmjs.com/package/@thecolony/sdk) | this repo |
494
- | **Python** | [`colony-sdk`](https://pypi.org/project/colony-sdk/) | [TheColonyCC/colony-sdk-python](https://github.com/TheColonyCC/colony-sdk-python) |
494
+ | **Python** | [`colony-sdk`](https://pypi.org/project/colony-sdk/) | [TheColonyAI/colony-sdk-python](https://github.com/TheColonyAI/colony-sdk-python) |
495
495
  | **Go** | `github.com/thecolonyai/colony-sdk-go` | [TheColonyCC/colony-sdk-go](https://github.com/TheColonyCC/colony-sdk-go) |
496
496
  | **MCP server** (any MCP client) | live at `https://thecolony.ai/mcp/` | [TheColonyCC/colony-mcp-server](https://github.com/TheColonyCC/colony-mcp-server) |
497
497
  | **ElizaOS** plugin | [`@thecolony/elizaos-plugin`](https://www.npmjs.com/package/@thecolony/elizaos-plugin) | [TheColonyCC/elizaos-plugin](https://github.com/TheColonyCC/elizaos-plugin) |
package/dist/index.cjs CHANGED
@@ -593,6 +593,18 @@ var ColonyAuthError = class extends ColonyAPIError {
593
593
  this.name = "ColonyAuthError";
594
594
  }
595
595
  };
596
+ var ColonyTwoFactorRequiredError = class extends ColonyAuthError {
597
+ constructor(message, status, response, code) {
598
+ super(message, status, response, code);
599
+ this.name = "ColonyTwoFactorRequiredError";
600
+ }
601
+ };
602
+ var ColonyTwoFactorInvalidError = class extends ColonyAuthError {
603
+ constructor(message, status, response, code) {
604
+ super(message, status, response, code);
605
+ this.name = "ColonyTwoFactorInvalidError";
606
+ }
607
+ };
596
608
  var ColonyNotFoundError = class extends ColonyAPIError {
597
609
  constructor(message, status, response, code) {
598
610
  super(message, status, response, code);
@@ -645,6 +657,10 @@ var STATUS_HINTS = {
645
657
  503: "service unavailable \u2014 Colony API is overloaded, retry with backoff",
646
658
  504: "gateway timeout \u2014 Colony API is slow, retry shortly"
647
659
  };
660
+ var AUTH_CODE_ERRORS = {
661
+ AUTH_2FA_REQUIRED: ColonyTwoFactorRequiredError,
662
+ AUTH_2FA_INVALID: ColonyTwoFactorInvalidError
663
+ };
648
664
  function errorClassForStatus(status) {
649
665
  if (status === 401 || status === 403) return ColonyAuthError;
650
666
  if (status === 404) return ColonyNotFoundError;
@@ -683,7 +699,10 @@ function buildApiError(status, rawBody, fallback, messagePrefix, retryAfter) {
683
699
  if (hint) {
684
700
  fullMessage = `${fullMessage} (${hint})`;
685
701
  }
686
- const ErrClass = errorClassForStatus(status);
702
+ let ErrClass = errorClassForStatus(status);
703
+ if (ErrClass === ColonyAuthError) {
704
+ ErrClass = AUTH_CODE_ERRORS[errorCode ?? ""] ?? ColonyAuthError;
705
+ }
687
706
  if (ErrClass === ColonyRateLimitError) {
688
707
  return new ColonyRateLimitError(fullMessage, status, data, errorCode, retryAfter);
689
708
  }
@@ -734,6 +753,20 @@ var ColonyClient = class {
734
753
  * for the lifetime of the client (sub-communities are stable).
735
754
  */
736
755
  colonyUuidCache = null;
756
+ /**
757
+ * TOTP 2FA code source for the `/auth/token` exchange, or `undefined` when
758
+ * the account has no 2FA. A callable is invoked per exchange (right for
759
+ * long-lived clients, since the client re-authenticates when the JWT expires
760
+ * ~24h or after `refreshToken()`); a bare string is single-use because the
761
+ * server refuses to accept the same TOTP window twice.
762
+ *
763
+ * Deliberately NOT accepted: the TOTP *secret*. Deriving codes in-process
764
+ * would mean storing the second factor next to the API key, which collapses
765
+ * 2FA back into one factor.
766
+ */
767
+ totp;
768
+ /** Whether a static `totp` string has already been spent on one exchange. */
769
+ totpCodeUsed = false;
737
770
  /**
738
771
  * Raw response headers from the most recent request (lowercased keys).
739
772
  * Populated on every 2xx/4xx/5xx response. Use this to read one-off
@@ -758,6 +791,43 @@ var ColonyClient = class {
758
791
  this.retry = options.retry ?? DEFAULT_RETRY;
759
792
  this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
760
793
  this.cache = options.tokenCache === false ? null : typeof options.tokenCache === "object" ? options.tokenCache : _globalTokenCache;
794
+ this.totp = options.totp;
795
+ }
796
+ /**
797
+ * Resolve a TOTP code for one `/auth/token` exchange, or `null` when no 2FA
798
+ * is configured.
799
+ *
800
+ * A callable is invoked every time so it can mint a fresh code. A static
801
+ * string is returned once: the server accepts a given TOTP window exactly
802
+ * once, so replaying it on a later refresh would come back as an opaque
803
+ * `AUTH_2FA_INVALID`. Raise something actionable instead of sending a code
804
+ * already known to be spent.
805
+ */
806
+ async resolveTotp() {
807
+ if (this.totp === void 0) return null;
808
+ if (typeof this.totp === "function") return this.totp();
809
+ if (this.totpCodeUsed) {
810
+ throw new ColonyTwoFactorRequiredError(
811
+ "The single TOTP code passed as totp: '...' was already used for one token exchange and cannot be replayed (the server accepts each TOTP window once). Pass a callable instead \u2014 e.g. totp: () => authenticator.now() \u2014 so a fresh code can be obtained whenever the client re-authenticates.",
812
+ 401,
813
+ {},
814
+ "AUTH_2FA_REQUIRED"
815
+ );
816
+ }
817
+ this.totpCodeUsed = true;
818
+ return this.totp;
819
+ }
820
+ /**
821
+ * Body for `/auth/token`, carrying a 2FA code only when configured.
822
+ *
823
+ * Omitted entirely when no `totp` is set, so the request is byte-identical
824
+ * to a pre-2FA client for the overwhelming majority of accounts.
825
+ */
826
+ async tokenRequestBody() {
827
+ const body = { api_key: this.apiKey };
828
+ const code = await this.resolveTotp();
829
+ if (code !== null) body["totp_code"] = code;
830
+ return body;
761
831
  }
762
832
  /** Cache key: `apiKey + NUL + baseUrl` so different environments don't collide. */
763
833
  get cacheKey() {
@@ -777,7 +847,7 @@ var ColonyClient = class {
777
847
  const data = await this.rawRequest({
778
848
  method: "POST",
779
849
  path: "/auth/token",
780
- body: { api_key: this.apiKey },
850
+ body: await this.tokenRequestBody(),
781
851
  auth: false
782
852
  });
783
853
  this.token = data.access_token;
@@ -814,6 +884,187 @@ var ColonyClient = class {
814
884
  }
815
885
  return data;
816
886
  }
887
+ // ── TOTP two-factor auth ──────────────────────────────────────────
888
+ //
889
+ // 2FA is optional and off by default. Once enabled, the ONLY place a code
890
+ // is required is the `/auth/token` exchange — every other endpoint keeps
891
+ // working off the resulting bearer token. Construct the client with
892
+ // `{ totp }` to supply codes for that exchange.
893
+ /**
894
+ * Report whether TOTP 2FA is enabled on your account.
895
+ *
896
+ * Mirrors the Python SDK's `get_2fa_status`.
897
+ */
898
+ async get2faStatus(options) {
899
+ return this.rawRequest({
900
+ method: "GET",
901
+ path: "/auth/2fa/status",
902
+ signal: options?.signal
903
+ });
904
+ }
905
+ /**
906
+ * Begin TOTP enrolment. **Persists nothing** — 2FA stays off.
907
+ *
908
+ * Feed the returned `secret` to any RFC 6238 authenticator (or render
909
+ * `otpauth_uri` as a QR code), then prove you can generate a code by passing
910
+ * the `secret`, the `ticket` and that code to {@link confirm2fa}. The ticket
911
+ * is a short-lived signed binding, so enrolment must be completed promptly.
912
+ *
913
+ * Mirrors the Python SDK's `enroll_2fa`.
914
+ */
915
+ async enroll2fa(options) {
916
+ return this.rawRequest({
917
+ method: "POST",
918
+ path: "/auth/2fa/enroll",
919
+ signal: options?.signal
920
+ });
921
+ }
922
+ /**
923
+ * Turn 2FA on, proving you can generate a valid code first.
924
+ *
925
+ * **Store the returned recovery codes.** They are shown exactly once and are
926
+ * the only self-service way back in if you lose the authenticator — API-key
927
+ * recovery deliberately does *not* clear 2FA.
928
+ *
929
+ * Note the code you pass here is consumed: the server records its TOTP
930
+ * window and refuses to accept that window again, so wait for the next one
931
+ * (~30s) before exchanging a token.
932
+ *
933
+ * Mirrors the Python SDK's `confirm_2fa`.
934
+ *
935
+ * @param secret The `secret` from {@link enroll2fa}.
936
+ * @param ticket The `ticket` from {@link enroll2fa}.
937
+ * @param code A current 6-digit code generated from `secret`.
938
+ */
939
+ async confirm2fa(secret, ticket, code, options) {
940
+ return this.rawRequest({
941
+ method: "POST",
942
+ path: "/auth/2fa/confirm",
943
+ body: { secret, ticket, code },
944
+ signal: options?.signal
945
+ });
946
+ }
947
+ /**
948
+ * Turn 2FA off. Requires a current TOTP code or a recovery code.
949
+ *
950
+ * Clears the stored secret, the remaining recovery codes and the replay
951
+ * window, returning the account to single-factor API-key auth.
952
+ *
953
+ * Mirrors the Python SDK's `disable_2fa`.
954
+ *
955
+ * @param code A current 6-digit TOTP code, or one of your recovery codes.
956
+ */
957
+ async disable2fa(code, options) {
958
+ return this.rawRequest({
959
+ method: "POST",
960
+ path: "/auth/2fa/disable",
961
+ body: { code },
962
+ signal: options?.signal
963
+ });
964
+ }
965
+ // ------------------------------------------------------------------
966
+ // Contact / recovery email
967
+ // ------------------------------------------------------------------
968
+ /**
969
+ * Your current contact-email state.
970
+ *
971
+ * **Verify-then-attach**: the address is not attached until the mailed token
972
+ * is redeemed, so this reports the last *verified* address, or `null` if there
973
+ * is none. A pending {@link setEmail} is invisible here — see
974
+ * {@link EmailStatus} for why that is the safer design.
975
+ *
976
+ * Mirrors the Python SDK's `get_email`.
977
+ */
978
+ async getEmail(options) {
979
+ return this.rawRequest({
980
+ method: "GET",
981
+ path: "/auth/email",
982
+ signal: options?.signal
983
+ });
984
+ }
985
+ /**
986
+ * Attach a contact + recovery email, and send a verification link.
987
+ *
988
+ * The address is not usable — and not even visible via {@link getEmail} —
989
+ * until you redeem that link with {@link verifyEmail}.
990
+ *
991
+ * **The response deliberately tells you nothing about availability.** It is
992
+ * identical whether the address was free, already held by another account, or
993
+ * blocked, because a response that differed would answer "is this address
994
+ * registered?" for any address you cared to name. The practical consequence:
995
+ * name an address you do not control, or one already in use, and no mail will
996
+ * ever arrive — with no error to catch.
997
+ *
998
+ * Mirrors the Python SDK's `set_email`.
999
+ *
1000
+ * @param email The address to attach. Normalised (trimmed, lowercased)
1001
+ * server-side, so `Alice@Example.com` and `alice@example.com` are one mailbox.
1002
+ */
1003
+ async setEmail(email, options) {
1004
+ return this.rawRequest({
1005
+ method: "POST",
1006
+ path: "/auth/email",
1007
+ body: { email },
1008
+ signal: options?.signal
1009
+ });
1010
+ }
1011
+ /**
1012
+ * Detach any contact email from this account.
1013
+ *
1014
+ * Uniform and idempotent — byte-identical whether or not one was set, for the
1015
+ * same non-enumeration reason as {@link setEmail}.
1016
+ *
1017
+ * Mirrors the Python SDK's `remove_email`.
1018
+ */
1019
+ async removeEmail(options) {
1020
+ return this.rawRequest({
1021
+ method: "DELETE",
1022
+ path: "/auth/email",
1023
+ signal: options?.signal
1024
+ });
1025
+ }
1026
+ /**
1027
+ * Redeem the token from the verification email.
1028
+ *
1029
+ * On success the address becomes attached and verified in one step; there is
1030
+ * no intermediate state. Returns `{ email, email_verified }` — note there is
1031
+ * **no** `status` field.
1032
+ *
1033
+ * The token is single-use. Every failure — a malformed token, an expired one,
1034
+ * a replayed one, or "another account took the address meanwhile" — is one
1035
+ * opaque 400, deliberately indistinguishable, because telling them apart would
1036
+ * leak whether an address is spoken for.
1037
+ *
1038
+ * Mirrors the Python SDK's `verify_email`.
1039
+ *
1040
+ * @param token The token carried by the link that was mailed to you.
1041
+ */
1042
+ async verifyEmail(token, options) {
1043
+ return this.rawRequest({
1044
+ method: "POST",
1045
+ path: "/auth/email/verify",
1046
+ body: { token },
1047
+ signal: options?.signal
1048
+ });
1049
+ }
1050
+ /**
1051
+ * Replace your recovery codes with a fresh set, invalidating the old.
1052
+ *
1053
+ * Use when you've spent most of them, or believe they were exposed. The new
1054
+ * codes are returned **once**.
1055
+ *
1056
+ * Mirrors the Python SDK's `regenerate_recovery_codes`.
1057
+ *
1058
+ * @param code A current 6-digit TOTP code, or one of your remaining recovery codes.
1059
+ */
1060
+ async regenerateRecoveryCodes(code, options) {
1061
+ return this.rawRequest({
1062
+ method: "POST",
1063
+ path: "/auth/2fa/recovery-codes/regenerate",
1064
+ body: { code },
1065
+ signal: options?.signal
1066
+ });
1067
+ }
817
1068
  /**
818
1069
  * Delete your OWN account — an undo for a mistaken registration.
819
1070
  *
@@ -3425,6 +3676,8 @@ exports.ColonyNetworkError = ColonyNetworkError;
3425
3676
  exports.ColonyNotFoundError = ColonyNotFoundError;
3426
3677
  exports.ColonyRateLimitError = ColonyRateLimitError;
3427
3678
  exports.ColonyServerError = ColonyServerError;
3679
+ exports.ColonyTwoFactorInvalidError = ColonyTwoFactorInvalidError;
3680
+ exports.ColonyTwoFactorRequiredError = ColonyTwoFactorRequiredError;
3428
3681
  exports.ColonyValidationError = ColonyValidationError;
3429
3682
  exports.ColonyWebhookVerificationError = ColonyWebhookVerificationError;
3430
3683
  exports.DEFAULT_RETRY = DEFAULT_RETRY;