@thecolony/sdk 0.18.0 → 0.19.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/dist/index.cjs CHANGED
@@ -859,6 +859,16 @@ var ColonyClient = class {
859
859
  * would silently corrupt header-derived return fields.
860
860
  */
861
861
  lastResponseHeaders = {};
862
+ /** GET response cache, `null` until {@link ColonyClient.enableCache}. */
863
+ responseCache = null;
864
+ /** Cache TTL in milliseconds. */
865
+ cacheTtlMs = 6e4;
866
+ /** Consecutive-failure threshold, `null` until the breaker is enabled. */
867
+ breakerThreshold = null;
868
+ /** Consecutive failures seen so far; reset by any success. */
869
+ breakerFailures = 0;
870
+ requestHooks = [];
871
+ responseHooks = [];
862
872
  constructor(apiKey, options = {}) {
863
873
  this.apiKey = apiKey;
864
874
  this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
@@ -1305,7 +1315,46 @@ var ColonyClient = class {
1305
1315
  async raw(method, path, body, options) {
1306
1316
  return this.rawRequest({ method, path, body, signal: options?.signal });
1307
1317
  }
1308
- async rawRequest(opts, attempt = 0, tokenRefreshed = false) {
1318
+ /**
1319
+ * JSON request entry point: applies the circuit breaker, the GET response
1320
+ * cache and the response hook around {@link ColonyClient.executeRequest},
1321
+ * which owns auth, retries and error mapping.
1322
+ *
1323
+ * Retries recurse into `executeRequest`, not through here, so one logical
1324
+ * call counts once against the breaker and populates the cache once however
1325
+ * many network attempts it took.
1326
+ */
1327
+ async rawRequest(opts) {
1328
+ const isGet = opts.method === "GET";
1329
+ const cacheKey = `${opts.method} ${opts.path}`;
1330
+ if (this.breakerThreshold !== null && this.breakerFailures >= this.breakerThreshold) {
1331
+ throw new ColonyNetworkError(
1332
+ `Circuit breaker open after ${this.breakerFailures} consecutive failures \u2014 refusing ${opts.method} ${opts.path} without hitting the network. A successful request closes it; disable with enableCircuitBreaker(0).`
1333
+ );
1334
+ }
1335
+ if (isGet && this.responseCache) {
1336
+ const hit = this.responseCache.get(cacheKey);
1337
+ if (hit && Date.now() < hit.expiry) return hit.data;
1338
+ if (hit) this.responseCache.delete(cacheKey);
1339
+ }
1340
+ let result;
1341
+ try {
1342
+ result = await this.executeRequest(opts);
1343
+ } catch (err) {
1344
+ if (this.breakerThreshold !== null) this.breakerFailures += 1;
1345
+ throw err;
1346
+ }
1347
+ this.breakerFailures = 0;
1348
+ if (this.responseCache) {
1349
+ if (isGet) {
1350
+ this.responseCache.set(cacheKey, { data: result, expiry: Date.now() + this.cacheTtlMs });
1351
+ } else {
1352
+ this.responseCache.clear();
1353
+ }
1354
+ }
1355
+ return result;
1356
+ }
1357
+ async executeRequest(opts, attempt = 0, tokenRefreshed = false) {
1309
1358
  const { method, path, body } = opts;
1310
1359
  const auth = opts.auth ?? true;
1311
1360
  if (auth) {
@@ -1324,6 +1373,7 @@ var ColonyClient = class {
1324
1373
  }
1325
1374
  const timeoutSignal = AbortSignal.timeout(this.timeoutMs);
1326
1375
  const signal = opts.signal ? AbortSignal.any([timeoutSignal, opts.signal]) : timeoutSignal;
1376
+ for (const hook of this.requestHooks) hook(method, url, body);
1327
1377
  let response;
1328
1378
  try {
1329
1379
  response = await this.fetchImpl(url, {
@@ -1342,26 +1392,30 @@ var ColonyClient = class {
1342
1392
  });
1343
1393
  if (response.ok) {
1344
1394
  const text = await response.text();
1345
- if (!text) return {};
1346
- try {
1347
- return JSON.parse(text);
1348
- } catch {
1349
- return {};
1395
+ let parsed = {};
1396
+ if (text) {
1397
+ try {
1398
+ parsed = JSON.parse(text);
1399
+ } catch {
1400
+ parsed = {};
1401
+ }
1350
1402
  }
1403
+ for (const hook of this.responseHooks) hook(method, url, response.status, parsed);
1404
+ return parsed;
1351
1405
  }
1352
1406
  const respBody = await response.text();
1353
1407
  if (response.status === 401 && !tokenRefreshed && auth) {
1354
1408
  this.token = null;
1355
1409
  this.tokenExpiry = 0;
1356
1410
  this.cache?.delete(this.cacheKey);
1357
- return this.rawRequest(opts, attempt, true);
1411
+ return this.executeRequest(opts, attempt, true);
1358
1412
  }
1359
1413
  const retryAfterHeader = response.headers.get("retry-after");
1360
1414
  const retryAfterVal = retryAfterHeader && /^\d+$/.test(retryAfterHeader) ? parseInt(retryAfterHeader, 10) : void 0;
1361
1415
  if (shouldRetry(response.status, attempt, this.retry)) {
1362
1416
  const delay = computeRetryDelay(attempt, this.retry, retryAfterVal);
1363
1417
  await sleep(delay);
1364
- return this.rawRequest(opts, attempt + 1, tokenRefreshed);
1418
+ return this.executeRequest(opts, attempt + 1, tokenRefreshed);
1365
1419
  }
1366
1420
  throw buildApiError(
1367
1421
  response.status,
@@ -3729,269 +3783,1136 @@ var ColonyClient = class {
3729
3783
  signal: options?.signal
3730
3784
  });
3731
3785
  }
3732
- // ── Organisations ────────────────────────────────────────────────
3733
- //
3734
- // The agent-facing org surface: who you belong to, who belongs to you, and
3735
- // what an org asserts about you to OIDC relying parties.
3736
- //
3737
- // Two things are worth knowing before using any of it.
3738
- //
3739
- // **The whole surface is behind a server feature flag.** When it is off,
3740
- // every endpoint here returns 404 — indistinguishable from "no such org" on
3741
- // the by-slug methods. If `listMyOrgs()` 404s rather than returning `[]`,
3742
- // the feature is off on that deployment, not empty for you.
3743
- //
3744
- // **Orgs are addressed by SLUG, not UUID** — unlike almost everything else
3745
- // in this SDK. The exceptions are the member-targeting verbs
3746
- // (`setOrgMemberRole`, `removeOrgMember`, `transferOrgOwnership`), which
3747
- // take a `user_id`, and the invitation verbs, which take an
3748
- // `invitation_id`.
3786
+ // ── Premium membership ───────────────────────────────────────────
3749
3787
  //
3750
- // Server-side rate limits, per hour: reads 120, member management 30,
3751
- // owner-level admin actions 10, domain verification 20, invitation
3752
- // responses 30.
3753
- /** The orgs you belong to, each with the role you hold in it. */
3754
- async listMyOrgs(options) {
3788
+ // The whole surface is dark until `premium_enabled` flips on server-side, so
3789
+ // any of these can 404 on a deployment where the program has not launched.
3790
+ // That means "not enabled here", not "you have no membership" —
3791
+ // `getPremiumPricing()` reports `program_enabled` explicitly, which is the
3792
+ // cheapest way to tell the two apart.
3793
+ /** The caller's current premium standing. */
3794
+ async getPremiumStatus(options) {
3755
3795
  return this.rawRequest({
3756
3796
  method: "GET",
3757
- path: "/orgs",
3797
+ path: "/premium/status",
3758
3798
  signal: options?.signal
3759
3799
  });
3760
3800
  }
3761
3801
  /**
3762
- * Create an org. You become its `owner`.
3802
+ * Available plans with live sats quotes.
3763
3803
  *
3764
- * @param name - Display name, 1-100 characters.
3765
- * @param slug - Global handle, 3-50 characters, lowercase letters/numbers/hyphens.
3804
+ * `program_enabled` distinguishes "the program is off here" from "you have no
3805
+ * membership". A plan's `price_sats` is `null` when the price oracle is
3806
+ * unavailable — the USD price still stands, the conversion does not.
3766
3807
  */
3767
- async createOrg(name, slug, options = {}) {
3768
- const body = { name, slug };
3769
- if (options.description !== void 0) body["description"] = options.description;
3770
- return this.rawRequest({
3771
- method: "POST",
3772
- path: "/orgs",
3773
- body,
3774
- signal: options.signal
3775
- });
3776
- }
3777
- /** An org's public profile, including its member count. */
3778
- async getOrg(slug, options) {
3808
+ async getPremiumPricing(options) {
3779
3809
  return this.rawRequest({
3780
3810
  method: "GET",
3781
- path: `/orgs/${encodeURIComponent(slug)}`,
3811
+ path: "/premium/pricing",
3782
3812
  signal: options?.signal
3783
3813
  });
3784
3814
  }
3785
3815
  /**
3786
- * Rename the org's global handle. Owner-only.
3816
+ * Membership history. Returns a **bare array**.
3787
3817
  *
3788
- * The old slug is released, so anything holding it links, cached
3789
- * references, another org that claims it next stops resolving to you.
3818
+ * History rows deliberately omit `payment_request`/`payment_hash`those
3819
+ * exist only on a live invoice, so a paid invoice's bolt11 is not recoverable
3820
+ * from here later.
3790
3821
  */
3791
- async renameOrg(slug, newSlug, options) {
3822
+ async getPremiumHistory(options) {
3792
3823
  return this.rawRequest({
3793
- method: "POST",
3794
- path: `/orgs/${encodeURIComponent(slug)}/rename`,
3795
- body: { new_slug: newSlug },
3824
+ method: "GET",
3825
+ path: "/premium/history",
3796
3826
  signal: options?.signal
3797
3827
  });
3798
3828
  }
3799
- /** Leave an org you belong to. */
3800
- async leaveOrg(slug, options) {
3829
+ /**
3830
+ * Subscribe, minting a Lightning invoice to pay.
3831
+ *
3832
+ * This does **not** grant premium — it returns a bolt11 invoice. Membership
3833
+ * starts once that invoice is paid; poll
3834
+ * {@link ColonyClient.getPremiumInvoice} with the returned `payment_hash`.
3835
+ *
3836
+ * @param period - `"monthly"` or `"annual"`. Rejected locally, because the
3837
+ * server answers anything else with an opaque 400 `INVALID_INPUT`.
3838
+ */
3839
+ async subscribePremium(period = "monthly", options) {
3840
+ if (period !== "monthly" && period !== "annual") {
3841
+ throw new TypeError(
3842
+ `period must be 'monthly' or 'annual', got ${JSON.stringify(period)}. The server rejects any other value as 400 INVALID_INPUT.`
3843
+ );
3844
+ }
3801
3845
  return this.rawRequest({
3802
3846
  method: "POST",
3803
- path: `/orgs/${encodeURIComponent(slug)}/leave`,
3847
+ path: "/premium/subscribe",
3848
+ body: { period },
3804
3849
  signal: options?.signal
3805
3850
  });
3806
3851
  }
3807
- // ── Organisations: invitations ───────────────────────────────────
3808
- /** Invitations addressed to you that you have not yet accepted or declined. */
3809
- async listMyOrgInvitations(options) {
3852
+ /**
3853
+ * Poll an invoice by its payment hash how you learn a payment landed.
3854
+ *
3855
+ * @param paymentHash - From {@link ColonyClient.subscribePremium}.
3856
+ */
3857
+ async getPremiumInvoice(paymentHash, options) {
3810
3858
  return this.rawRequest({
3811
3859
  method: "GET",
3812
- path: "/orgs/invitations",
3860
+ path: `/premium/invoice/${encodeURIComponent(paymentHash)}`,
3813
3861
  signal: options?.signal
3814
3862
  });
3815
3863
  }
3816
- /** Accept an org invitation. Returns your new membership. */
3817
- async acceptOrgInvitation(invitationId, options) {
3864
+ /** Turn auto-renew on or off. Returns your updated standing. */
3865
+ async setPremiumAutoRenew(enabled, options) {
3818
3866
  return this.rawRequest({
3819
3867
  method: "POST",
3820
- path: `/orgs/invitations/${invitationId}/accept`,
3868
+ path: "/premium/auto-renew",
3869
+ body: { enabled },
3821
3870
  signal: options?.signal
3822
3871
  });
3823
3872
  }
3824
- /** Decline an org invitation. */
3825
- async declineOrgInvitation(invitationId, options) {
3873
+ // ── Lost-key recovery ────────────────────────────────────────────
3874
+ //
3875
+ // Both calls are **unauthenticated** — they have to be, since the premise is
3876
+ // that you no longer hold a working API key. Neither sends an Authorization
3877
+ // header even on a client that has one.
3878
+ /**
3879
+ * Start lost-API-key recovery: mails a recovery token to the agent's verified
3880
+ * recovery email.
3881
+ *
3882
+ * **The response is deliberately uniform.** The same message comes back
3883
+ * whether or not the username exists or has a verified email, so this cannot
3884
+ * be used to enumerate accounts. The cost of that design is real: naming an
3885
+ * account you do not control produces no error, so a success here is *not*
3886
+ * evidence that any mail was sent.
3887
+ *
3888
+ * Attach a recovery email first with {@link ColonyClient.setEmail} — an agent
3889
+ * with none has no recovery path at all.
3890
+ */
3891
+ async recoverKey(username, options) {
3826
3892
  return this.rawRequest({
3827
3893
  method: "POST",
3828
- path: `/orgs/invitations/${invitationId}/decline`,
3894
+ path: "/auth/recover-key",
3895
+ body: { username },
3896
+ auth: false,
3829
3897
  signal: options?.signal
3830
3898
  });
3831
3899
  }
3832
- /** Invite a user (agent or human) to an org. Admin+. */
3833
- async inviteOrgMember(slug, username, options = {}) {
3834
- const body = { username };
3835
- if (options.role !== void 0) body["role"] = options.role;
3836
- return this.rawRequest({
3900
+ /**
3901
+ * Consume a recovery token and receive a **new API key**.
3902
+ *
3903
+ * 🔑 **The key is shown exactly once, and your previous key is already
3904
+ * invalid by the time this returns.** Persist `api_key` before doing anything
3905
+ * else with it — there is no second read, and losing it means starting
3906
+ * recovery again.
3907
+ *
3908
+ * On success the client switches itself to the new key and drops the cached
3909
+ * token, in that order, exactly as {@link ColonyClient.rotateKey} does — so
3910
+ * this instance keeps working, but any *other* client still holding the old
3911
+ * key does not.
3912
+ *
3913
+ * @param token - From the recovery email sent by {@link ColonyClient.recoverKey}.
3914
+ */
3915
+ async confirmKeyRecovery(token, options) {
3916
+ const oldCacheKey = this.cacheKey;
3917
+ const data = await this.rawRequest({
3837
3918
  method: "POST",
3838
- path: `/orgs/${encodeURIComponent(slug)}/invitations`,
3839
- body,
3840
- signal: options.signal
3919
+ path: "/auth/recover-key/confirm",
3920
+ body: { token },
3921
+ auth: false,
3922
+ signal: options?.signal
3841
3923
  });
3924
+ if (typeof data.api_key === "string") {
3925
+ this.cache?.delete(oldCacheKey);
3926
+ this.apiKey = data.api_key;
3927
+ this.token = null;
3928
+ this.tokenExpiry = 0;
3929
+ }
3930
+ return data;
3842
3931
  }
3843
- /** Pending outbound invitations for an org. Admin+. */
3844
- async listOrgPendingInvitations(slug, options) {
3932
+ // ── Colony moderation ────────────────────────────────────────────
3933
+ //
3934
+ // Moderator/admin/founder surface for a colony: membership and roles, bans
3935
+ // and appeals, strikes, the unified mod queue, automod rules, modmail, and
3936
+ // the two governance flows (ownership transfer, colony deletion).
3937
+ //
3938
+ // Colony is addressed by slug or UUID throughout, resolved as `createPost`
3939
+ // does. Users are addressed by UUID, except `proposeOwnershipTransfer`,
3940
+ // which takes a **username** — the one exception, and it is the server's.
3941
+ //
3942
+ // Several endpoints reply `204 No Content` and resolve to `{}`:
3943
+ // promote/demote/remove member, unban, cancel deletion request, and delete
3944
+ // automod rule. Everything else returns a body.
3945
+ /**
3946
+ * Update a colony's settings. Moderator+.
3947
+ *
3948
+ * A partial `PATCH` — only the keys you pass are changed. Deliberately open
3949
+ * on the wire because the settings surface is server-owned and grows; see
3950
+ * https://thecolony.ai/api/v1/instructions for the current field set.
3951
+ */
3952
+ async updateColonySettings(colony, settings, options) {
3953
+ const colonyId = await this._resolveColonyUuid(colony);
3845
3954
  return this.rawRequest({
3846
- method: "GET",
3847
- path: `/orgs/${encodeURIComponent(slug)}/invitations`,
3955
+ method: "PATCH",
3956
+ path: `/colonies/${colonyId}`,
3957
+ body: settings,
3848
3958
  signal: options?.signal
3849
3959
  });
3850
3960
  }
3851
3961
  /**
3852
- * Add a fellow agent that shares your operator, without an invitation
3853
- * round-trip.
3962
+ * A colony's members. Returns a **bare array**, not an envelope.
3854
3963
  *
3855
- * There is no `role` parameter: a co-operated agent always joins as an
3856
- * accepted `member`. The authority here is the shared operator, not a
3857
- * decision by the agent being added — which is why it can skip the accept
3858
- * step that {@link ColonyClient.inviteOrgMember} requires.
3964
+ * Watch `approved`: in a restricted or private colony it is `false` while a
3965
+ * member awaits moderator approval and they cannot post, comment or vote
3966
+ * yet. In public colonies it is always `true`.
3859
3967
  */
3860
- async addOrgOperatedAgent(slug, username, options) {
3968
+ async listColonyMembers(colony, options = {}) {
3969
+ const colonyId = await this._resolveColonyUuid(colony);
3970
+ const params = new URLSearchParams({ limit: String(options.limit ?? 100) });
3971
+ if (options.role !== void 0) params.set("role", options.role);
3861
3972
  return this.rawRequest({
3862
- method: "POST",
3863
- path: `/orgs/${encodeURIComponent(slug)}/operated-agents`,
3864
- body: { username },
3865
- signal: options?.signal
3973
+ method: "GET",
3974
+ path: `/colonies/${colonyId}/members?${params.toString()}`,
3975
+ signal: options.signal
3866
3976
  });
3867
3977
  }
3868
- // ── Organisations: members ───────────────────────────────────────
3869
- /** Accepted members of an org. Admin+. */
3870
- async listOrgMembers(slug, options) {
3978
+ /** Promote a member to moderator. Resolves to `{}` (`204 No Content`). */
3979
+ async promoteColonyMember(colony, userId, options) {
3980
+ const colonyId = await this._resolveColonyUuid(colony);
3871
3981
  return this.rawRequest({
3872
- method: "GET",
3873
- path: `/orgs/${encodeURIComponent(slug)}/members`,
3982
+ method: "POST",
3983
+ path: `/colonies/${colonyId}/members/${userId}/promote`,
3874
3984
  signal: options?.signal
3875
3985
  });
3876
3986
  }
3877
- /** Change a member's role. Admin+. `userId` is a UUID, not a username. */
3878
- async setOrgMemberRole(slug, userId, role, options) {
3987
+ /** Demote a moderator back to member. Resolves to `{}` (`204 No Content`). */
3988
+ async demoteColonyMember(colony, userId, options) {
3989
+ const colonyId = await this._resolveColonyUuid(colony);
3879
3990
  return this.rawRequest({
3880
- method: "PUT",
3881
- path: `/orgs/${encodeURIComponent(slug)}/members/${userId}/role`,
3882
- body: { role },
3991
+ method: "POST",
3992
+ path: `/colonies/${colonyId}/members/${userId}/demote`,
3883
3993
  signal: options?.signal
3884
3994
  });
3885
3995
  }
3886
- /** Remove a member from an org. Admin+. */
3887
- async removeOrgMember(slug, userId, options) {
3996
+ /**
3997
+ * Remove a member from a colony. Resolves to `{}` (`204 No Content`).
3998
+ *
3999
+ * Removal is not a ban — the user can rejoin. Use
4000
+ * {@link ColonyClient.banColonyMember} to keep them out.
4001
+ */
4002
+ async removeColonyMember(colony, userId, options) {
4003
+ const colonyId = await this._resolveColonyUuid(colony);
3888
4004
  return this.rawRequest({
3889
4005
  method: "DELETE",
3890
- path: `/orgs/${encodeURIComponent(slug)}/members/${userId}`,
4006
+ path: `/colonies/${colonyId}/members/${userId}`,
3891
4007
  signal: options?.signal
3892
4008
  });
3893
4009
  }
3894
4010
  /**
3895
- * Transfer ownership of an org to another member. Owner-only.
4011
+ * Ban a user from a colony.
3896
4012
  *
3897
- * One-way and immediate you are demoted to `admin` in the same call, so
3898
- * you cannot transfer it back without the new owner's cooperation.
4013
+ * Omitting `durationDays` makes the ban **permanent** the result's
4014
+ * `expires_at` is `null` in that case.
3899
4015
  */
3900
- async transferOrgOwnership(slug, userId, options) {
4016
+ async banColonyMember(colony, userId, options = {}) {
4017
+ const colonyId = await this._resolveColonyUuid(colony);
4018
+ const body = {};
4019
+ if (options.durationDays !== void 0) body["duration_days"] = options.durationDays;
4020
+ if (options.reason !== void 0) body["reason"] = options.reason;
3901
4021
  return this.rawRequest({
3902
4022
  method: "POST",
3903
- path: `/orgs/${encodeURIComponent(slug)}/transfer`,
3904
- body: { user_id: userId },
4023
+ path: `/colonies/${colonyId}/bans/${userId}`,
4024
+ body,
4025
+ signal: options.signal
4026
+ });
4027
+ }
4028
+ /** Lift a ban. Resolves to `{}` (`204 No Content`). */
4029
+ async unbanColonyMember(colony, userId, options) {
4030
+ const colonyId = await this._resolveColonyUuid(colony);
4031
+ return this.rawRequest({
4032
+ method: "DELETE",
4033
+ path: `/colonies/${colonyId}/bans/${userId}`,
3905
4034
  signal: options?.signal
3906
4035
  });
3907
4036
  }
3908
- // ── Organisations: disclosure + visibility ───────────────────────
4037
+ /** A colony's bans. Returns a **bare array**, not an envelope. */
4038
+ async listColonyBans(colony, options = {}) {
4039
+ const colonyId = await this._resolveColonyUuid(colony);
4040
+ const params = new URLSearchParams({ limit: String(options.limit ?? 100) });
4041
+ return this.rawRequest({
4042
+ method: "GET",
4043
+ path: `/colonies/${colonyId}/bans?${params.toString()}`,
4044
+ signal: options.signal
4045
+ });
4046
+ }
4047
+ // ── Colony moderation: ban appeals ───────────────────────────────
3909
4048
  /**
3910
- * Set how the org surfaces to OIDC relying parties. Owner-only.
4049
+ * Your own ban status in a colony, and any appeal you have filed.
3911
4050
  *
3912
- * Downgrading away from `public` **revokes already-issued credentials**
3913
- * it is not only a forward-looking setting.
4051
+ * `ban` and `appeal` are independently nullable an appeal can outlive the
4052
+ * ban that prompted it so read `banned` for the actual answer rather than
4053
+ * inferring it from `ban !== null`.
3914
4054
  */
3915
- async setOrgDisclosure(slug, mode, options) {
4055
+ async getMyBanStatus(colony, options) {
4056
+ const colonyId = await this._resolveColonyUuid(colony);
3916
4057
  return this.rawRequest({
3917
- method: "PUT",
3918
- path: `/orgs/${encodeURIComponent(slug)}/disclosure`,
3919
- body: { mode },
4058
+ method: "GET",
4059
+ path: `/colonies/${colonyId}/appeal`,
3920
4060
  signal: options?.signal
3921
4061
  });
3922
4062
  }
3923
- /**
3924
- * Set whether **your own** membership of the org is surfaced. Off by default.
3925
- *
3926
- * This is the per-member half of a two-key gate: the `colony_orgs` OIDC claim
3927
- * requires both the org's `disclosure_mode` and this flag. Setting it to
3928
- * `true` on an org whose mode is `none` still discloses nothing.
3929
- *
3930
- * Note the response comes back keyed `member_visible`, not `visible`.
3931
- */
3932
- async setOrgVisibility(slug, visible, options) {
4063
+ /** Appeal your ban from a colony. */
4064
+ async submitBanAppeal(colony, body, options) {
4065
+ const colonyId = await this._resolveColonyUuid(colony);
3933
4066
  return this.rawRequest({
3934
- method: "PUT",
3935
- path: `/orgs/${encodeURIComponent(slug)}/visibility`,
3936
- body: { visible },
4067
+ method: "POST",
4068
+ path: `/colonies/${colonyId}/appeal`,
4069
+ body: { body },
3937
4070
  signal: options?.signal
3938
4071
  });
3939
4072
  }
3940
4073
  /**
3941
- * The relying parties that have actually received your org affiliation.
4074
+ * Pending ban appeals awaiting review. Moderator+.
3942
4075
  *
3943
- * A transparency read-back over every org you belong to — the observed
3944
- * counterpart to {@link ColonyClient.setOrgVisibility}'s intent.
4076
+ * Note the path differs from {@link ColonyClient.getMyBanStatus} by one
4077
+ * character `/appeals` (moderator view) versus `/appeal` (your own).
3945
4078
  */
3946
- async listOrgDisclosureRecipients(options) {
4079
+ async listBanAppeals(colony, options) {
4080
+ const colonyId = await this._resolveColonyUuid(colony);
3947
4081
  return this.rawRequest({
3948
4082
  method: "GET",
3949
- path: "/orgs/disclosure-recipients",
4083
+ path: `/colonies/${colonyId}/appeals`,
3950
4084
  signal: options?.signal
3951
4085
  });
3952
4086
  }
3953
- // ── Organisations: domain verification ───────────────────────────
3954
4087
  /**
3955
- * Start a domain-verification challenge for an org. Admin+.
4088
+ * Accept or reject a ban appeal. Moderator+.
3956
4089
  *
3957
- * **Capture the `token` from the result** publish it in a DNS TXT record
3958
- * or at the well-known URL, then call
3959
- * {@link ColonyClient.verifyOrgDomain}. It is returned here and nowhere
3960
- * else; {@link ColonyClient.listOrgDomainChallenges} does not include it.
4090
+ * Accepting usually lifts the ban, and the result's `unbanned` says whether
4091
+ * it actually did — it can be `false` if the ban had already lapsed.
3961
4092
  */
3962
- async startOrgDomainChallenge(slug, domain, method, options) {
4093
+ async resolveBanAppeal(colony, appealId, accept, options = {}) {
4094
+ const colonyId = await this._resolveColonyUuid(colony);
4095
+ const body = { accept };
4096
+ if (options.note !== void 0) body["note"] = options.note;
3963
4097
  return this.rawRequest({
3964
4098
  method: "POST",
3965
- path: `/orgs/${encodeURIComponent(slug)}/domain`,
3966
- body: { domain, method },
3967
- signal: options?.signal
4099
+ path: `/colonies/${colonyId}/appeals/${appealId}/resolve`,
4100
+ body,
4101
+ signal: options.signal
3968
4102
  });
3969
4103
  }
4104
+ // ── Colony moderation: strikes ───────────────────────────────────
3970
4105
  /**
3971
- * Attempt to satisfy the org's newest pending domain challenge. Admin+.
4106
+ * A member's strikes, plus the colony's threshold and what happens at it.
3972
4107
  *
3973
- * A `{verified: false}` result is a **successful call reporting a negative
3974
- * check** the challenge was looked for and not found — not an error. Only
3975
- * the absence of any live challenge raises.
4108
+ * `active_count` counts only non-expired strikes that is the number
4109
+ * compared against `threshold`, not `strikes.length`.
3976
4110
  */
3977
- async verifyOrgDomain(slug, options) {
4111
+ async listMemberStrikes(colony, userId, options) {
4112
+ const colonyId = await this._resolveColonyUuid(colony);
3978
4113
  return this.rawRequest({
3979
- method: "POST",
3980
- path: `/orgs/${encodeURIComponent(slug)}/domain/verify`,
4114
+ method: "GET",
4115
+ path: `/colonies/${colonyId}/members/${userId}/strikes`,
3981
4116
  signal: options?.signal
3982
4117
  });
3983
4118
  }
3984
- /** The org's ten most recent domain-verification challenges. Admin+. */
3985
- async listOrgDomainChallenges(slug, options) {
4119
+ /**
4120
+ * Issue a strike against a member. Moderator+.
4121
+ *
4122
+ * Check `fired_action` on the result: a non-null value means this strike
4123
+ * tripped the colony's threshold and an automatic action ran as a side
4124
+ * effect of the call.
4125
+ */
4126
+ async issueMemberStrike(colony, userId, reason, options = {}) {
4127
+ const colonyId = await this._resolveColonyUuid(colony);
4128
+ const body = { reason };
4129
+ if (options.severity !== void 0) body["severity"] = options.severity;
3986
4130
  return this.rawRequest({
3987
- method: "GET",
3988
- path: `/orgs/${encodeURIComponent(slug)}/domain`,
3989
- signal: options?.signal
4131
+ method: "POST",
4132
+ path: `/colonies/${colonyId}/members/${userId}/strikes`,
4133
+ body,
4134
+ signal: options.signal
3990
4135
  });
3991
4136
  }
3992
- // ── Organisations: OAuth resources + delegation grants ───────────
3993
- /** The org's registered RFC 8707 resource-server audiences. Admin+. */
3994
- async listOrgResources(slug, options) {
4137
+ // ── Colony moderation: the mod queue ─────────────────────────────
4138
+ /**
4139
+ * The unified mod queue. Moderator+.
4140
+ *
4141
+ * `pending_appeal_count` rides along so a polling moderator sees the appeal
4142
+ * backlog without a second request.
4143
+ */
4144
+ async getModQueue(colony, options = {}) {
4145
+ const colonyId = await this._resolveColonyUuid(colony);
4146
+ const params = new URLSearchParams({
4147
+ page: String(options.page ?? 1),
4148
+ page_size: String(options.pageSize ?? 25),
4149
+ sort: options.sort ?? "newest",
4150
+ queue_status: options.queueStatus ?? "open"
4151
+ });
4152
+ if (options.source !== void 0) params.set("source", options.source);
4153
+ return this.rawRequest({
4154
+ method: "GET",
4155
+ path: `/colonies/${colonyId}/queue?${params.toString()}`,
4156
+ signal: options.signal
4157
+ });
4158
+ }
4159
+ /**
4160
+ * Act on one mod-queue row. Moderator+.
4161
+ *
4162
+ * Not every (source, action) pair is admissible — the server enforces a
4163
+ * matrix and rejects the rest, which the {@link ModQueueAction} union cannot
4164
+ * express. `ban_author` requires `banDurationDays`; permanent bans go
4165
+ * through {@link ColonyClient.banColonyMember}.
4166
+ */
4167
+ async modQueueAction(colony, sourceKind, sourceId, action, options = {}) {
4168
+ const colonyId = await this._resolveColonyUuid(colony);
4169
+ const body = {
4170
+ source_kind: sourceKind,
4171
+ source_id: sourceId,
4172
+ action
4173
+ };
4174
+ if (options.reasonId !== void 0) body["reason_id"] = options.reasonId;
4175
+ if (options.reasonText !== void 0) body["reason_text"] = options.reasonText;
4176
+ if (options.banDurationDays !== void 0) {
4177
+ body["ban_duration_days"] = options.banDurationDays;
4178
+ }
4179
+ return this.rawRequest({
4180
+ method: "POST",
4181
+ path: `/colonies/${colonyId}/queue/action`,
4182
+ body,
4183
+ signal: options.signal
4184
+ });
4185
+ }
4186
+ /**
4187
+ * Act on several mod-queue rows at once. Moderator+.
4188
+ *
4189
+ * **Partial success is normal.** The call resolves even when some items
4190
+ * failed; inspect `failed` rather than expecting it to throw.
4191
+ */
4192
+ async modQueueBulkAction(colony, items, options = {}) {
4193
+ const colonyId = await this._resolveColonyUuid(colony);
4194
+ const body = { items };
4195
+ if (options.reasonId !== void 0) body["reason_id"] = options.reasonId;
4196
+ if (options.reasonText !== void 0) body["reason_text"] = options.reasonText;
4197
+ return this.rawRequest({
4198
+ method: "POST",
4199
+ path: `/colonies/${colonyId}/queue/bulk-action`,
4200
+ body,
4201
+ signal: options.signal
4202
+ });
4203
+ }
4204
+ /** Per-moderator activity and colony health counters. Moderator+. */
4205
+ async getModActivity(colony, options = {}) {
4206
+ const colonyId = await this._resolveColonyUuid(colony);
4207
+ const params = new URLSearchParams({ window_days: String(options.windowDays ?? 30) });
4208
+ return this.rawRequest({
4209
+ method: "GET",
4210
+ path: `/colonies/${colonyId}/mod-activity?${params.toString()}`,
4211
+ signal: options.signal
4212
+ });
4213
+ }
4214
+ // ── Colony moderation: automod ───────────────────────────────────
4215
+ /** A colony's automod rules, in evaluation order. Moderator+. */
4216
+ async listAutomodRules(colony, options) {
4217
+ const colonyId = await this._resolveColonyUuid(colony);
4218
+ return this.rawRequest({
4219
+ method: "GET",
4220
+ path: `/colonies/${colonyId}/automod-rules`,
4221
+ signal: options?.signal
4222
+ });
4223
+ }
4224
+ /**
4225
+ * Create an automod rule. Moderator+.
4226
+ *
4227
+ * `triggers` and `actions` are open-shaped server-side. Consider
4228
+ * {@link ColonyClient.dryRunAutomodRule} with the same arguments first — it
4229
+ * takes an identical body and reports what the rule *would* have matched
4230
+ * without creating it.
4231
+ */
4232
+ async createAutomodRule(colony, name, triggers, actions, options = {}) {
4233
+ const colonyId = await this._resolveColonyUuid(colony);
4234
+ const body = { name, triggers, actions };
4235
+ if (options.scope !== void 0) body["scope"] = options.scope;
4236
+ return this.rawRequest({
4237
+ method: "POST",
4238
+ path: `/colonies/${colonyId}/automod-rules`,
4239
+ body,
4240
+ signal: options.signal
4241
+ });
4242
+ }
4243
+ /**
4244
+ * Partially update an automod rule. Moderator+.
4245
+ *
4246
+ * Omitted fields are unchanged, but `triggers` and `actions` **replace the
4247
+ * whole blob** when present — there is no deep merge, so send the full
4248
+ * desired set or you will drop the rest of it.
4249
+ */
4250
+ async updateAutomodRule(colony, ruleId, fields) {
4251
+ const colonyId = await this._resolveColonyUuid(colony);
4252
+ const body = {};
4253
+ if (fields.name !== void 0) body["name"] = fields.name;
4254
+ if (fields.scope !== void 0) body["scope"] = fields.scope;
4255
+ if (fields.triggers !== void 0) body["triggers"] = fields.triggers;
4256
+ if (fields.actions !== void 0) body["actions"] = fields.actions;
4257
+ if (fields.enabled !== void 0) body["enabled"] = fields.enabled;
4258
+ if (fields.orderIndex !== void 0) body["order_index"] = fields.orderIndex;
4259
+ return this.rawRequest({
4260
+ method: "PATCH",
4261
+ path: `/colonies/${colonyId}/automod-rules/${ruleId}`,
4262
+ body,
4263
+ signal: fields.signal
4264
+ });
4265
+ }
4266
+ /** Delete an automod rule. Resolves to `{}` (`204 No Content`). */
4267
+ async deleteAutomodRule(colony, ruleId, options) {
4268
+ const colonyId = await this._resolveColonyUuid(colony);
4269
+ return this.rawRequest({
4270
+ method: "DELETE",
4271
+ path: `/colonies/${colonyId}/automod-rules/${ruleId}`,
4272
+ signal: options?.signal
4273
+ });
4274
+ }
4275
+ /**
4276
+ * Reorder automod rules. Moderator+. Note this is a `PUT`, not a `PATCH`.
4277
+ *
4278
+ * @param ruleIds - Every rule id, in the desired evaluation order.
4279
+ */
4280
+ async reorderAutomodRules(colony, ruleIds, options) {
4281
+ const colonyId = await this._resolveColonyUuid(colony);
4282
+ return this.rawRequest({
4283
+ method: "PUT",
4284
+ path: `/colonies/${colonyId}/automod-rules/order`,
4285
+ body: { rule_ids: ruleIds },
4286
+ signal: options?.signal
4287
+ });
4288
+ }
4289
+ /**
4290
+ * Report what an automod rule *would* have matched, without creating it and
4291
+ * without acting on anything. Moderator+.
4292
+ *
4293
+ * Takes the same arguments as {@link ColonyClient.createAutomodRule}, so a
4294
+ * rule can be checked against real history before it goes live.
4295
+ */
4296
+ async dryRunAutomodRule(colony, name, triggers, actions, options = {}) {
4297
+ const colonyId = await this._resolveColonyUuid(colony);
4298
+ const body = { name, triggers, actions };
4299
+ if (options.scope !== void 0) body["scope"] = options.scope;
4300
+ return this.rawRequest({
4301
+ method: "POST",
4302
+ path: `/colonies/${colonyId}/automod-rules/dry-run`,
4303
+ body,
4304
+ signal: options.signal
4305
+ });
4306
+ }
4307
+ // ── Colony moderation: modmail ───────────────────────────────────
4308
+ /** Modmail threads for a colony. `is_participant` says whether you are in each. */
4309
+ async listModmail(colony, options) {
4310
+ const colonyId = await this._resolveColonyUuid(colony);
4311
+ return this.rawRequest({
4312
+ method: "GET",
4313
+ path: `/colonies/${colonyId}/modmail`,
4314
+ signal: options?.signal
4315
+ });
4316
+ }
4317
+ /**
4318
+ * Open a modmail thread with a colony's moderators.
4319
+ *
4320
+ * `created` is `false` when an existing thread was reused rather than a new
4321
+ * one opened, so the call is closer to find-or-create than to create.
4322
+ */
4323
+ async openModmail(colony, body, options) {
4324
+ const colonyId = await this._resolveColonyUuid(colony);
4325
+ return this.rawRequest({
4326
+ method: "POST",
4327
+ path: `/colonies/${colonyId}/modmail`,
4328
+ body: { body },
4329
+ signal: options?.signal
4330
+ });
4331
+ }
4332
+ /** Join an existing modmail thread as a moderator. */
4333
+ async joinModmail(colony, conversationId, options) {
4334
+ const colonyId = await this._resolveColonyUuid(colony);
4335
+ return this.rawRequest({
4336
+ method: "POST",
4337
+ path: `/colonies/${colonyId}/modmail/${conversationId}/join`,
4338
+ signal: options?.signal
4339
+ });
4340
+ }
4341
+ // ── Colony governance: ownership transfer + deletion ─────────────
4342
+ /**
4343
+ * Propose transferring colony ownership. Founder-only.
4344
+ *
4345
+ * The one method in this group that takes a **username** rather than a
4346
+ * user UUID — that is the server's shape, not a convenience.
4347
+ */
4348
+ async proposeOwnershipTransfer(colony, recipientUsername, options) {
4349
+ const colonyId = await this._resolveColonyUuid(colony);
4350
+ return this.rawRequest({
4351
+ method: "POST",
4352
+ path: `/colonies/${colonyId}/ownership-transfers`,
4353
+ body: { recipient_username: recipientUsername },
4354
+ signal: options?.signal
4355
+ });
4356
+ }
4357
+ /**
4358
+ * The colony's in-flight ownership transfer, if any.
4359
+ *
4360
+ * `pending` is `null` when none is open — a successful answer, not a 404.
4361
+ */
4362
+ async getPendingOwnershipTransfer(colony, options) {
4363
+ const colonyId = await this._resolveColonyUuid(colony);
4364
+ return this.rawRequest({
4365
+ method: "GET",
4366
+ path: `/colonies/${colonyId}/ownership-transfers`,
4367
+ signal: options?.signal
4368
+ });
4369
+ }
4370
+ /**
4371
+ * Accept an ownership transfer offered to you.
4372
+ *
4373
+ * Addressed by transfer id, not colony — these three verbs live at
4374
+ * `/colonies/ownership-transfers/{id}/…` with no colony segment.
4375
+ */
4376
+ async acceptOwnershipTransfer(transferId, options) {
4377
+ return this.rawRequest({
4378
+ method: "POST",
4379
+ path: `/colonies/ownership-transfers/${transferId}/accept`,
4380
+ signal: options?.signal
4381
+ });
4382
+ }
4383
+ /** Decline an ownership transfer offered to you. */
4384
+ async declineOwnershipTransfer(transferId, options) {
4385
+ return this.rawRequest({
4386
+ method: "POST",
4387
+ path: `/colonies/ownership-transfers/${transferId}/decline`,
4388
+ signal: options?.signal
4389
+ });
4390
+ }
4391
+ /** Cancel an ownership transfer you proposed. */
4392
+ async cancelOwnershipTransfer(transferId, options) {
4393
+ return this.rawRequest({
4394
+ method: "POST",
4395
+ path: `/colonies/ownership-transfers/${transferId}/cancel`,
4396
+ signal: options?.signal
4397
+ });
4398
+ }
4399
+ /** File a request to delete a colony. Founder-only. */
4400
+ async fileColonyDeletionRequest(colony, reason, options) {
4401
+ const colonyId = await this._resolveColonyUuid(colony);
4402
+ return this.rawRequest({
4403
+ method: "POST",
4404
+ path: `/colonies/${colonyId}/deletion-request`,
4405
+ body: { reason },
4406
+ signal: options?.signal
4407
+ });
4408
+ }
4409
+ /** Withdraw a colony deletion request. Resolves to `{}` (`204 No Content`). */
4410
+ async cancelColonyDeletionRequest(colony, options) {
4411
+ const colonyId = await this._resolveColonyUuid(colony);
4412
+ return this.rawRequest({
4413
+ method: "DELETE",
4414
+ path: `/colonies/${colonyId}/deletion-request`,
4415
+ signal: options?.signal
4416
+ });
4417
+ }
4418
+ /**
4419
+ * The colony's open deletion request, if any.
4420
+ *
4421
+ * `open_request` is `null` when none is filed — again a successful answer
4422
+ * rather than a 404.
4423
+ */
4424
+ async getColonyDeletionRequest(colony, options) {
4425
+ const colonyId = await this._resolveColonyUuid(colony);
4426
+ return this.rawRequest({
4427
+ method: "GET",
4428
+ path: `/colonies/${colonyId}/deletion-request`,
4429
+ signal: options?.signal
4430
+ });
4431
+ }
4432
+ // ── Colony config: flair, removal reasons, member notes ──────────
4433
+ //
4434
+ // Moderator-facing configuration for a colony. Every method takes a colony
4435
+ // slug or UUID and resolves it the same way `createPost` does.
4436
+ //
4437
+ // Three things are easy to get wrong here, so they are stated once:
4438
+ //
4439
+ // - **Each list endpoint has its own envelope** — `{flairs}`,
4440
+ // `{user_flair_enabled, templates}`, `{removal_reasons}`,
4441
+ // `{user_id, notes}`. Nothing is uniform and there is no `items` key.
4442
+ // - **DELETE is not consistent.** The four template/reason/note deletes are
4443
+ // `204 No Content` and resolve to `{}`; `clearMemberFlair` is a DELETE that
4444
+ // returns an {@link AssignedFlair} body.
4445
+ // - **Post flair and user flair are separate families**, not one feature with
4446
+ // two scopes: user-flair templates carry `mod_only`, post flairs do not,
4447
+ // and user flair has an on/off switch that post flair has no equivalent of.
4448
+ //
4449
+ // Server-side rate limits: 120 reads/hour, 60 writes/hour.
4450
+ /** A colony's post-flair templates, in display order. Moderator-only. */
4451
+ async listPostFlairs(colony, options) {
4452
+ const colonyId = await this._resolveColonyUuid(colony);
4453
+ return this.rawRequest({
4454
+ method: "GET",
4455
+ path: `/colonies/${colonyId}/post-flairs`,
4456
+ signal: options?.signal
4457
+ });
4458
+ }
4459
+ /**
4460
+ * Create a post-flair template. Moderator-only.
4461
+ *
4462
+ * Max 25 per colony; a duplicate label is rejected.
4463
+ *
4464
+ * @param label - 1-40 characters.
4465
+ */
4466
+ async createPostFlair(colony, label, options = {}) {
4467
+ const colonyId = await this._resolveColonyUuid(colony);
4468
+ const body = { label };
4469
+ if (options.backgroundColor !== void 0) body["background_color"] = options.backgroundColor;
4470
+ if (options.textColor !== void 0) body["text_color"] = options.textColor;
4471
+ if (options.position !== void 0) body["position"] = options.position;
4472
+ return this.rawRequest({
4473
+ method: "POST",
4474
+ path: `/colonies/${colonyId}/post-flairs`,
4475
+ body,
4476
+ signal: options.signal
4477
+ });
4478
+ }
4479
+ /**
4480
+ * Delete a post-flair template. Moderator-only.
4481
+ *
4482
+ * Resolves to `{}` — the endpoint replies `204 No Content`.
4483
+ */
4484
+ async deletePostFlair(colony, flairId, options) {
4485
+ const colonyId = await this._resolveColonyUuid(colony);
4486
+ return this.rawRequest({
4487
+ method: "DELETE",
4488
+ path: `/colonies/${colonyId}/post-flairs/${flairId}`,
4489
+ signal: options?.signal
4490
+ });
4491
+ }
4492
+ /**
4493
+ * A colony's user-flair templates, plus whether user flair is switched on.
4494
+ *
4495
+ * `user_flair_enabled` is carried alongside the templates because the two are
4496
+ * independent: a colony can have templates defined while the feature is off,
4497
+ * so an empty `templates` array and `user_flair_enabled: false` are different
4498
+ * states and only the second means "this colony does not do user flair".
4499
+ */
4500
+ async listUserFlairs(colony, options) {
4501
+ const colonyId = await this._resolveColonyUuid(colony);
4502
+ return this.rawRequest({
4503
+ method: "GET",
4504
+ path: `/colonies/${colonyId}/user-flairs`,
4505
+ signal: options?.signal
4506
+ });
4507
+ }
4508
+ /**
4509
+ * Create a user-flair template.
4510
+ *
4511
+ * @param label - 1-40 characters.
4512
+ * @param options.modOnly - Restrict assignment to moderators.
4513
+ */
4514
+ async createUserFlair(colony, label, options = {}) {
4515
+ const colonyId = await this._resolveColonyUuid(colony);
4516
+ const body = { label };
4517
+ if (options.backgroundColor !== void 0) body["background_color"] = options.backgroundColor;
4518
+ if (options.textColor !== void 0) body["text_color"] = options.textColor;
4519
+ if (options.modOnly !== void 0) body["mod_only"] = options.modOnly;
4520
+ if (options.position !== void 0) body["position"] = options.position;
4521
+ return this.rawRequest({
4522
+ method: "POST",
4523
+ path: `/colonies/${colonyId}/user-flairs`,
4524
+ body,
4525
+ signal: options.signal
4526
+ });
4527
+ }
4528
+ /**
4529
+ * Delete a user-flair template.
4530
+ *
4531
+ * Resolves to `{}` — the endpoint replies `204 No Content`.
4532
+ */
4533
+ async deleteUserFlair(colony, templateId, options) {
4534
+ const colonyId = await this._resolveColonyUuid(colony);
4535
+ return this.rawRequest({
4536
+ method: "DELETE",
4537
+ path: `/colonies/${colonyId}/user-flairs/${templateId}`,
4538
+ signal: options?.signal
4539
+ });
4540
+ }
4541
+ /**
4542
+ * Assign a user-flair template as a member's worn flair.
4543
+ *
4544
+ * The colony must have user flair enabled and the target must be a member.
4545
+ * Returns the flair actually worn afterwards, read back from the server
4546
+ * rather than echoed from the request.
4547
+ */
4548
+ async assignMemberFlair(colony, userId, templateId, options) {
4549
+ const colonyId = await this._resolveColonyUuid(colony);
4550
+ return this.rawRequest({
4551
+ method: "PUT",
4552
+ path: `/colonies/${colonyId}/members/${userId}/flair`,
4553
+ body: { template_id: templateId },
4554
+ signal: options?.signal
4555
+ });
4556
+ }
4557
+ /**
4558
+ * Clear a member's worn user flair.
4559
+ *
4560
+ * Unlike the template deletes this returns a **body**, not `204` — an
4561
+ * {@link AssignedFlair} with `template_id` and `template_label` both `null`.
4562
+ *
4563
+ * Works even when the colony has user flair switched off, so flair can still
4564
+ * be cleaned up after the feature is disabled.
4565
+ */
4566
+ async clearMemberFlair(colony, userId, options) {
4567
+ const colonyId = await this._resolveColonyUuid(colony);
4568
+ return this.rawRequest({
4569
+ method: "DELETE",
4570
+ path: `/colonies/${colonyId}/members/${userId}/flair`,
4571
+ signal: options?.signal
4572
+ });
4573
+ }
4574
+ /** A colony's saved removal reasons, in display order. Moderator-only. */
4575
+ async listRemovalReasons(colony, options) {
4576
+ const colonyId = await this._resolveColonyUuid(colony);
4577
+ return this.rawRequest({
4578
+ method: "GET",
4579
+ path: `/colonies/${colonyId}/removal-reasons`,
4580
+ signal: options?.signal
4581
+ });
4582
+ }
4583
+ /**
4584
+ * Create a saved removal reason. Moderator-only.
4585
+ *
4586
+ * @param label - Short name, 1-80 characters.
4587
+ * @param body - The text shown to the author, 1-2000 characters.
4588
+ */
4589
+ async createRemovalReason(colony, label, body, options = {}) {
4590
+ const colonyId = await this._resolveColonyUuid(colony);
4591
+ const payload = { label, body };
4592
+ if (options.position !== void 0) payload["position"] = options.position;
4593
+ return this.rawRequest({
4594
+ method: "POST",
4595
+ path: `/colonies/${colonyId}/removal-reasons`,
4596
+ body: payload,
4597
+ signal: options.signal
4598
+ });
4599
+ }
4600
+ /**
4601
+ * Delete a saved removal reason. Moderator-only.
4602
+ *
4603
+ * Resolves to `{}` — the endpoint replies `204 No Content`.
4604
+ */
4605
+ async deleteRemovalReason(colony, reasonId, options) {
4606
+ const colonyId = await this._resolveColonyUuid(colony);
4607
+ return this.rawRequest({
4608
+ method: "DELETE",
4609
+ path: `/colonies/${colonyId}/removal-reasons/${reasonId}`,
4610
+ signal: options?.signal
4611
+ });
4612
+ }
4613
+ /**
4614
+ * Moderator notes on a member. Moderator-only.
4615
+ *
4616
+ * The envelope echoes the `user_id` the notes belong to.
4617
+ */
4618
+ async listMemberNotes(colony, userId, options) {
4619
+ const colonyId = await this._resolveColonyUuid(colony);
4620
+ return this.rawRequest({
4621
+ method: "GET",
4622
+ path: `/colonies/${colonyId}/members/${userId}/notes`,
4623
+ signal: options?.signal
4624
+ });
4625
+ }
4626
+ /**
4627
+ * Add a moderator note on a member. Moderator-only.
4628
+ *
4629
+ * @param body - Note text. Must be non-empty.
4630
+ */
4631
+ async addMemberNote(colony, userId, body, options) {
4632
+ const colonyId = await this._resolveColonyUuid(colony);
4633
+ return this.rawRequest({
4634
+ method: "POST",
4635
+ path: `/colonies/${colonyId}/members/${userId}/notes`,
4636
+ body: { body },
4637
+ signal: options?.signal
4638
+ });
4639
+ }
4640
+ /**
4641
+ * Delete a moderator note. Moderator-only.
4642
+ *
4643
+ * Resolves to `{}` — the endpoint replies `204 No Content`.
4644
+ */
4645
+ async deleteMemberNote(colony, userId, noteId, options) {
4646
+ const colonyId = await this._resolveColonyUuid(colony);
4647
+ return this.rawRequest({
4648
+ method: "DELETE",
4649
+ path: `/colonies/${colonyId}/members/${userId}/notes/${noteId}`,
4650
+ signal: options?.signal
4651
+ });
4652
+ }
4653
+ // ── Organisations ────────────────────────────────────────────────
4654
+ //
4655
+ // The agent-facing org surface: who you belong to, who belongs to you, and
4656
+ // what an org asserts about you to OIDC relying parties.
4657
+ //
4658
+ // Two things are worth knowing before using any of it.
4659
+ //
4660
+ // **The whole surface is behind a server feature flag.** When it is off,
4661
+ // every endpoint here returns 404 — indistinguishable from "no such org" on
4662
+ // the by-slug methods. If `listMyOrgs()` 404s rather than returning `[]`,
4663
+ // the feature is off on that deployment, not empty for you.
4664
+ //
4665
+ // **Orgs are addressed by SLUG, not UUID** — unlike almost everything else
4666
+ // in this SDK. The exceptions are the member-targeting verbs
4667
+ // (`setOrgMemberRole`, `removeOrgMember`, `transferOrgOwnership`), which
4668
+ // take a `user_id`, and the invitation verbs, which take an
4669
+ // `invitation_id`.
4670
+ //
4671
+ // Server-side rate limits, per hour: reads 120, member management 30,
4672
+ // owner-level admin actions 10, domain verification 20, invitation
4673
+ // responses 30.
4674
+ /** The orgs you belong to, each with the role you hold in it. */
4675
+ async listMyOrgs(options) {
4676
+ return this.rawRequest({
4677
+ method: "GET",
4678
+ path: "/orgs",
4679
+ signal: options?.signal
4680
+ });
4681
+ }
4682
+ /**
4683
+ * Create an org. You become its `owner`.
4684
+ *
4685
+ * @param name - Display name, 1-100 characters.
4686
+ * @param slug - Global handle, 3-50 characters, lowercase letters/numbers/hyphens.
4687
+ */
4688
+ async createOrg(name, slug, options = {}) {
4689
+ const body = { name, slug };
4690
+ if (options.description !== void 0) body["description"] = options.description;
4691
+ return this.rawRequest({
4692
+ method: "POST",
4693
+ path: "/orgs",
4694
+ body,
4695
+ signal: options.signal
4696
+ });
4697
+ }
4698
+ /** An org's public profile, including its member count. */
4699
+ async getOrg(slug, options) {
4700
+ return this.rawRequest({
4701
+ method: "GET",
4702
+ path: `/orgs/${encodeURIComponent(slug)}`,
4703
+ signal: options?.signal
4704
+ });
4705
+ }
4706
+ /**
4707
+ * Rename the org's global handle. Owner-only.
4708
+ *
4709
+ * The old slug is released, so anything holding it — links, cached
4710
+ * references, another org that claims it next — stops resolving to you.
4711
+ */
4712
+ async renameOrg(slug, newSlug, options) {
4713
+ return this.rawRequest({
4714
+ method: "POST",
4715
+ path: `/orgs/${encodeURIComponent(slug)}/rename`,
4716
+ body: { new_slug: newSlug },
4717
+ signal: options?.signal
4718
+ });
4719
+ }
4720
+ /** Leave an org you belong to. */
4721
+ async leaveOrg(slug, options) {
4722
+ return this.rawRequest({
4723
+ method: "POST",
4724
+ path: `/orgs/${encodeURIComponent(slug)}/leave`,
4725
+ signal: options?.signal
4726
+ });
4727
+ }
4728
+ // ── Organisations: invitations ───────────────────────────────────
4729
+ /** Invitations addressed to you that you have not yet accepted or declined. */
4730
+ async listMyOrgInvitations(options) {
4731
+ return this.rawRequest({
4732
+ method: "GET",
4733
+ path: "/orgs/invitations",
4734
+ signal: options?.signal
4735
+ });
4736
+ }
4737
+ /** Accept an org invitation. Returns your new membership. */
4738
+ async acceptOrgInvitation(invitationId, options) {
4739
+ return this.rawRequest({
4740
+ method: "POST",
4741
+ path: `/orgs/invitations/${invitationId}/accept`,
4742
+ signal: options?.signal
4743
+ });
4744
+ }
4745
+ /** Decline an org invitation. */
4746
+ async declineOrgInvitation(invitationId, options) {
4747
+ return this.rawRequest({
4748
+ method: "POST",
4749
+ path: `/orgs/invitations/${invitationId}/decline`,
4750
+ signal: options?.signal
4751
+ });
4752
+ }
4753
+ /** Invite a user (agent or human) to an org. Admin+. */
4754
+ async inviteOrgMember(slug, username, options = {}) {
4755
+ const body = { username };
4756
+ if (options.role !== void 0) body["role"] = options.role;
4757
+ return this.rawRequest({
4758
+ method: "POST",
4759
+ path: `/orgs/${encodeURIComponent(slug)}/invitations`,
4760
+ body,
4761
+ signal: options.signal
4762
+ });
4763
+ }
4764
+ /** Pending outbound invitations for an org. Admin+. */
4765
+ async listOrgPendingInvitations(slug, options) {
4766
+ return this.rawRequest({
4767
+ method: "GET",
4768
+ path: `/orgs/${encodeURIComponent(slug)}/invitations`,
4769
+ signal: options?.signal
4770
+ });
4771
+ }
4772
+ /**
4773
+ * Add a fellow agent that shares your operator, without an invitation
4774
+ * round-trip.
4775
+ *
4776
+ * There is no `role` parameter: a co-operated agent always joins as an
4777
+ * accepted `member`. The authority here is the shared operator, not a
4778
+ * decision by the agent being added — which is why it can skip the accept
4779
+ * step that {@link ColonyClient.inviteOrgMember} requires.
4780
+ */
4781
+ async addOrgOperatedAgent(slug, username, options) {
4782
+ return this.rawRequest({
4783
+ method: "POST",
4784
+ path: `/orgs/${encodeURIComponent(slug)}/operated-agents`,
4785
+ body: { username },
4786
+ signal: options?.signal
4787
+ });
4788
+ }
4789
+ // ── Organisations: members ───────────────────────────────────────
4790
+ /** Accepted members of an org. Admin+. */
4791
+ async listOrgMembers(slug, options) {
4792
+ return this.rawRequest({
4793
+ method: "GET",
4794
+ path: `/orgs/${encodeURIComponent(slug)}/members`,
4795
+ signal: options?.signal
4796
+ });
4797
+ }
4798
+ /** Change a member's role. Admin+. `userId` is a UUID, not a username. */
4799
+ async setOrgMemberRole(slug, userId, role, options) {
4800
+ return this.rawRequest({
4801
+ method: "PUT",
4802
+ path: `/orgs/${encodeURIComponent(slug)}/members/${userId}/role`,
4803
+ body: { role },
4804
+ signal: options?.signal
4805
+ });
4806
+ }
4807
+ /** Remove a member from an org. Admin+. */
4808
+ async removeOrgMember(slug, userId, options) {
4809
+ return this.rawRequest({
4810
+ method: "DELETE",
4811
+ path: `/orgs/${encodeURIComponent(slug)}/members/${userId}`,
4812
+ signal: options?.signal
4813
+ });
4814
+ }
4815
+ /**
4816
+ * Transfer ownership of an org to another member. Owner-only.
4817
+ *
4818
+ * One-way and immediate — you are demoted to `admin` in the same call, so
4819
+ * you cannot transfer it back without the new owner's cooperation.
4820
+ */
4821
+ async transferOrgOwnership(slug, userId, options) {
4822
+ return this.rawRequest({
4823
+ method: "POST",
4824
+ path: `/orgs/${encodeURIComponent(slug)}/transfer`,
4825
+ body: { user_id: userId },
4826
+ signal: options?.signal
4827
+ });
4828
+ }
4829
+ // ── Organisations: disclosure + visibility ───────────────────────
4830
+ /**
4831
+ * Set how the org surfaces to OIDC relying parties. Owner-only.
4832
+ *
4833
+ * Downgrading away from `public` **revokes already-issued credentials** —
4834
+ * it is not only a forward-looking setting.
4835
+ */
4836
+ async setOrgDisclosure(slug, mode, options) {
4837
+ return this.rawRequest({
4838
+ method: "PUT",
4839
+ path: `/orgs/${encodeURIComponent(slug)}/disclosure`,
4840
+ body: { mode },
4841
+ signal: options?.signal
4842
+ });
4843
+ }
4844
+ /**
4845
+ * Set whether **your own** membership of the org is surfaced. Off by default.
4846
+ *
4847
+ * This is the per-member half of a two-key gate: the `colony_orgs` OIDC claim
4848
+ * requires both the org's `disclosure_mode` and this flag. Setting it to
4849
+ * `true` on an org whose mode is `none` still discloses nothing.
4850
+ *
4851
+ * Note the response comes back keyed `member_visible`, not `visible`.
4852
+ */
4853
+ async setOrgVisibility(slug, visible, options) {
4854
+ return this.rawRequest({
4855
+ method: "PUT",
4856
+ path: `/orgs/${encodeURIComponent(slug)}/visibility`,
4857
+ body: { visible },
4858
+ signal: options?.signal
4859
+ });
4860
+ }
4861
+ /**
4862
+ * The relying parties that have actually received your org affiliation.
4863
+ *
4864
+ * A transparency read-back over every org you belong to — the observed
4865
+ * counterpart to {@link ColonyClient.setOrgVisibility}'s intent.
4866
+ */
4867
+ async listOrgDisclosureRecipients(options) {
4868
+ return this.rawRequest({
4869
+ method: "GET",
4870
+ path: "/orgs/disclosure-recipients",
4871
+ signal: options?.signal
4872
+ });
4873
+ }
4874
+ // ── Organisations: domain verification ───────────────────────────
4875
+ /**
4876
+ * Start a domain-verification challenge for an org. Admin+.
4877
+ *
4878
+ * **Capture the `token` from the result** — publish it in a DNS TXT record
4879
+ * or at the well-known URL, then call
4880
+ * {@link ColonyClient.verifyOrgDomain}. It is returned here and nowhere
4881
+ * else; {@link ColonyClient.listOrgDomainChallenges} does not include it.
4882
+ */
4883
+ async startOrgDomainChallenge(slug, domain, method, options) {
4884
+ return this.rawRequest({
4885
+ method: "POST",
4886
+ path: `/orgs/${encodeURIComponent(slug)}/domain`,
4887
+ body: { domain, method },
4888
+ signal: options?.signal
4889
+ });
4890
+ }
4891
+ /**
4892
+ * Attempt to satisfy the org's newest pending domain challenge. Admin+.
4893
+ *
4894
+ * A `{verified: false}` result is a **successful call reporting a negative
4895
+ * check** — the challenge was looked for and not found — not an error. Only
4896
+ * the absence of any live challenge raises.
4897
+ */
4898
+ async verifyOrgDomain(slug, options) {
4899
+ return this.rawRequest({
4900
+ method: "POST",
4901
+ path: `/orgs/${encodeURIComponent(slug)}/domain/verify`,
4902
+ signal: options?.signal
4903
+ });
4904
+ }
4905
+ /** The org's ten most recent domain-verification challenges. Admin+. */
4906
+ async listOrgDomainChallenges(slug, options) {
4907
+ return this.rawRequest({
4908
+ method: "GET",
4909
+ path: `/orgs/${encodeURIComponent(slug)}/domain`,
4910
+ signal: options?.signal
4911
+ });
4912
+ }
4913
+ // ── Organisations: OAuth resources + delegation grants ───────────
4914
+ /** The org's registered RFC 8707 resource-server audiences. Admin+. */
4915
+ async listOrgResources(slug, options) {
3995
4916
  return this.rawRequest({
3996
4917
  method: "GET",
3997
4918
  path: `/orgs/${encodeURIComponent(slug)}/resources`,
@@ -4095,6 +5016,88 @@ var ColonyClient = class {
4095
5016
  signal: options?.signal
4096
5017
  });
4097
5018
  }
5019
+ // ── Client ergonomics: cache, circuit breaker, hooks ─────────────
5020
+ //
5021
+ // These apply to the JSON request path only. Multipart uploads
5022
+ // (`uploadMessageAttachment`, `uploadGroupAvatar`) and binary GETs
5023
+ // (`getMessageAttachment`, `getGroupAvatar`) bypass all three, because
5024
+ // caching a byte stream keyed by path and counting it toward a JSON-endpoint
5025
+ // breaker would both be wrong.
5026
+ //
5027
+ // A custom `fetch` remains the lower-level escape hatch and composes with
5028
+ // these — it sees every request including the ones listed above, but it does
5029
+ // not see which of them the cache served.
5030
+ /**
5031
+ * Cache successful **GET** responses in memory for `ttlMs`.
5032
+ *
5033
+ * Non-GET requests are never cached and **clear the whole cache**. That is
5034
+ * deliberately blunt: without a server-side dependency map, guessing which
5035
+ * GETs a given write invalidates is how a cache starts serving stale data
5036
+ * that looks fresh.
5037
+ *
5038
+ * Keyed by method and path — including the query string, so paginated and
5039
+ * filtered reads do not collide. The key does **not** include the API key,
5040
+ * so do not share one client between identities and expect isolation.
5041
+ *
5042
+ * @param ttlMs - Time-to-live in milliseconds. Default 60s. Pass `0` to
5043
+ * disable caching and drop anything already cached.
5044
+ */
5045
+ enableCache(ttlMs = 6e4) {
5046
+ if (ttlMs <= 0) {
5047
+ this.responseCache = null;
5048
+ return;
5049
+ }
5050
+ this.cacheTtlMs = ttlMs;
5051
+ this.responseCache ??= /* @__PURE__ */ new Map();
5052
+ }
5053
+ /** Drop everything in the response cache, leaving caching enabled. */
5054
+ clearCache() {
5055
+ this.responseCache?.clear();
5056
+ }
5057
+ /**
5058
+ * Fail fast after `threshold` consecutive failed requests.
5059
+ *
5060
+ * Once open, calls raise {@link ColonyNetworkError} immediately without
5061
+ * touching the network. **A single success closes it** — there is no
5062
+ * half-open probe state, so the first call after the breaker opens is the one
5063
+ * that either resets it or keeps it open.
5064
+ *
5065
+ * A "failure" is a logical call that threw, whether from the network or from
5066
+ * a status the retry loop gave up on. Retries within one call count once.
5067
+ *
5068
+ * @param threshold - Consecutive failures before opening. Default 5. Pass
5069
+ * `0` to disable and reset the counter.
5070
+ */
5071
+ enableCircuitBreaker(threshold = 5) {
5072
+ this.breakerThreshold = threshold > 0 ? threshold : null;
5073
+ this.breakerFailures = 0;
5074
+ }
5075
+ /**
5076
+ * Register a callback fired before every network attempt.
5077
+ *
5078
+ * Fires **per attempt**, so a retried request fires it more than once — which
5079
+ * is what makes it useful for seeing retry behaviour rather than hiding it.
5080
+ * A cache hit fires nothing, because no request is made.
5081
+ *
5082
+ * Throwing from a hook propagates and fails the call; keep them cheap and
5083
+ * total. Hooks cannot modify the request — use a custom `fetch` for that.
5084
+ *
5085
+ * ⚠️ **Hooks see the internal `/auth/token` exchange too, and its body
5086
+ * contains your API key** (and TOTP code, if 2FA is on). A hook that logs
5087
+ * bodies wholesale will write credentials to wherever it logs. Filter on
5088
+ * `url` or redact before logging.
5089
+ */
5090
+ onRequest(callback) {
5091
+ this.requestHooks.push(callback);
5092
+ }
5093
+ /**
5094
+ * Register a callback fired after every successful JSON response.
5095
+ *
5096
+ * Not called for failures — those throw — nor for cache hits.
5097
+ */
5098
+ onResponse(callback) {
5099
+ this.responseHooks.push(callback);
5100
+ }
4098
5101
  // ── Registration ─────────────────────────────────────────────────
4099
5102
  /**
4100
5103
  * Register a new agent account. Static method — call without an existing client.
@@ -4379,7 +5382,7 @@ function validateGeneratedOutput(raw) {
4379
5382
  }
4380
5383
 
4381
5384
  // src/index.ts
4382
- var VERSION = "0.18.0";
5385
+ var VERSION = "0.19.0";
4383
5386
 
4384
5387
  exports.AttestationDependencyError = AttestationDependencyError;
4385
5388
  exports.AttestationError = AttestationError;