@ixo/oracle-runtime-workers 0.33.0 → 0.33.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ixo/oracle-runtime-workers",
3
- "version": "0.33.0",
3
+ "version": "0.33.1",
4
4
  "description": "QiForge oracle runtime for Cloudflare Workers — Durable Object per user (SQLite in WASM over DO storage), Matrix E2EE gateway Durable Object, Hono HTTP/SSE shell.",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -34,7 +34,7 @@
34
34
  "!src/plugins/pod-creator/test-fixtures.ts"
35
35
  ],
36
36
  "dependencies": {
37
- "@ixo/action-protocol": "0.9.0",
37
+ "@ixo/action-protocol": "0.9.1",
38
38
  "@blocknote/server-util": "0.29.1",
39
39
  "@composio/core": "0.10.0",
40
40
  "@composio/langchain": "0.9.2",
@@ -68,8 +68,8 @@
68
68
  "@ixo/topic-protocol": "1.0.0-rc.8",
69
69
  "survey-core": "2.3.11",
70
70
  "@ixo/common": "1.7.1",
71
- "@ixo/ixo-transaction": "0.2.0",
72
- "@ixo/ucan": "2.3.0"
71
+ "@ixo/ucan": "2.3.0",
72
+ "@ixo/ixo-transaction": "0.2.0"
73
73
  },
74
74
  "devDependencies": {
75
75
  "@cloudflare/vitest-pool-workers": "^0.22.0",
@@ -115,12 +115,14 @@ export interface HubActionTransport {
115
115
  * An Auth Hub older than this runtime's version checks `?version=` against
116
116
  * its closed list (1 to 6 on 0.8.0, 1 to 5 on 0.7.0) and answers every call
117
117
  * that declares a version it does not know with 400 `invalid_request`,
118
- * before anything else. This runtime then steps down
118
+ * before anything else. This runtime's capabilities read then steps down
119
119
  * (`HUB_PROTOCOL_VERSIONS`: 7, then 6, then 5) to the highest version that
120
- * Hub answers (every Hub action the lower level has keeps working while
121
- * Auth Hub is rolled out: the group steps at 6, the level-6 chain types too
122
- * at 5) and asks for 7 again after `HUB_PROTOCOL_RECHECK_MS`; a Hub that
123
- * refuses 5 too is told apart with `HUB_UPDATE_REQUIRED`.
120
+ * Hub answers, and every other call uses that version (every Hub action
121
+ * the lower level has keeps working while Auth Hub is rolled out: the group
122
+ * steps at 6, the level-6 chain types too at 5); it asks for 7 again after
123
+ * `HUB_PROTOCOL_RECHECK_MS`. Another read refused at that version never
124
+ * lowers it (`atSettledVersion`). A Hub that refuses 5 too is told apart
125
+ * with `HUB_UPDATE_REQUIRED`.
124
126
  */
125
127
  const HUB_PROTOCOL_VERSION = '7';
126
128
  /** The lowest version this runtime works at: 7 without the group steps and the level-6 chain types. */
@@ -144,6 +146,14 @@ export const HUB_PROTOCOL_RECHECK_MS = 10 * 60_000;
144
146
  * callbacks pick the outcomes up once the Hub is updated.
145
147
  */
146
148
  export const HUB_UPDATE_REQUIRED = `Auth Hub must be updated before this oracle can use Hub actions: it does not accept protocol version ${HUB_PROTOCOL_LOWEST_VERSION}, the lowest this runtime works with (invalid_request). Tell the person this is not available right now; retrying or changing the fields will not help.`;
149
+ /**
150
+ * A read (options, status) Auth Hub refused at the version its
151
+ * capabilities read accepts: a fault on that route, not an older Hub.
152
+ */
153
+ export function hubReadRefused(version: string): string {
154
+ return `Auth Hub refused this read at protocol version ${version}, which it accepts for this oracle's other calls (invalid_request). Tell the person this is not available right now and try again later; changing the fields will not help.`;
155
+ }
156
+
147
157
  /** Every Hub call but a chain-transaction prepare. */
148
158
  const HUB_TIMEOUT_MS = 10_000;
149
159
  /**
@@ -198,12 +208,15 @@ export class HubActionClient {
198
208
  }
199
209
 
200
210
  /**
201
- * A read at the current version; when Auth Hub refuses it as unknown,
202
- * the same read at each lower version in turn (`HUB_PROTOCOL_VERSIONS`),
203
- * and the version it answered is remembered for
204
- * `HUB_PROTOCOL_RECHECK_MS`. Only reads do this: a refused read changed
205
- * nothing, while a prepare's 400 can be a real refusal of its operation.
206
- * A Hub that refuses the lowest version too answers `HUB_UPDATE_REQUIRED`.
211
+ * The capabilities read at the current version; when Auth Hub refuses it
212
+ * as unknown, the same read at each lower version in turn
213
+ * (`HUB_PROTOCOL_VERSIONS`), and the version it answered is remembered for
214
+ * `HUB_PROTOCOL_RECHECK_MS`. Only the capabilities read decides the
215
+ * version: a refused read changed nothing, while a prepare's 400 can be a
216
+ * real refusal of its operation, and another route refusing a version
217
+ * (an options route bug once refused 7 and 6) must never downgrade the
218
+ * operations. A Hub that refuses the lowest version too answers
219
+ * `HUB_UPDATE_REQUIRED`.
207
220
  */
208
221
  private async negotiated<T>(
209
222
  read: (version: HubProtocolVersion) => Promise<T>,
@@ -229,6 +242,37 @@ export class HubActionClient {
229
242
  throw new Error('No Hub protocol version to read at');
230
243
  }
231
244
 
245
+ /**
246
+ * Another read (options, status) at the version the capabilities read
247
+ * settled on. When Auth Hub refuses it as unknown, the capabilities read
248
+ * settles the version again (a Hub rolled back since); only when that
249
+ * changes the version is the read tried once more, at the new version.
250
+ * Otherwise this read fails on its own (`hubReadRefused`) and the version
251
+ * the operations use stays as it is. `timeoutMs` bounds that capabilities
252
+ * read as it bounds this one.
253
+ */
254
+ private async atSettledVersion<T>(
255
+ read: (version: HubProtocolVersion) => Promise<T>,
256
+ timeoutMs = HUB_TIMEOUT_MS,
257
+ ): Promise<T> {
258
+ const version = this.protocolVersion;
259
+ try {
260
+ return await read(version);
261
+ } catch (error) {
262
+ if (!isUpdateRequired(error)) throw error;
263
+ }
264
+ await this.capabilities(timeoutMs);
265
+ const settled = this.protocolVersion;
266
+ if (settled === version)
267
+ throw new HubActionRefused(
268
+ 'refused',
269
+ hubReadRefused(version),
270
+ undefined,
271
+ 400,
272
+ );
273
+ return read(settled);
274
+ }
275
+
232
276
  /**
233
277
  * `subject`: what a refused prepare is called in the model's words;
234
278
  * `steps`: a chain transaction's message types, for the words of a refusal
@@ -343,7 +387,7 @@ export class HubActionClient {
343
387
  // Options version 3 (asked at the protocol version; version 4 answers
344
388
  // the same): version 2 plus the owner's bank tokens
345
389
  // (`transfer.tokens`), which any-token transfers are chosen from.
346
- const options = await this.negotiated(async (version) => {
390
+ const options = await this.atSettledVersion(async (version) => {
347
391
  const query = new URLSearchParams({
348
392
  bindingId: input.bindingId,
349
393
  bindingRevision: String(input.bindingRevision),
@@ -391,12 +435,14 @@ export class HubActionClient {
391
435
  ): Promise<HubActionStatus> {
392
436
  if (!/^[A-Za-z0-9_-]{32}$/.test(id))
393
437
  throw new Error('Invalid Hub action id');
394
- const raw = await this.negotiated((version) =>
395
- this.send(
396
- `/api/internal/actions/operations/${id}?version=${version}`,
397
- undefined,
398
- timeoutMs,
399
- ),
438
+ const raw = await this.atSettledVersion(
439
+ (version) =>
440
+ this.send(
441
+ `/api/internal/actions/operations/${id}?version=${version}`,
442
+ undefined,
443
+ timeoutMs,
444
+ ),
445
+ timeoutMs,
400
446
  );
401
447
  if (!isRecord(raw)) throw new Error('Invalid Hub action status');
402
448
  const value = raw;
@@ -103,6 +103,13 @@ const SERVICE_REF =
103
103
  */
104
104
  const LINKED_ENTITY_ADDRESS =
105
105
  /^(?:\{id\}#)?ixo1(?:[023456789acdefghjklmnpqrstuvwxyz]{38}|[023456789acdefghjklmnpqrstuvwxyz]{58})$/;
106
+ /**
107
+ * Who receives coins or impact tokens: a 20-byte account or a 32-byte
108
+ * contract address (a group's own account, its treasury). Never a signer or
109
+ * identity field.
110
+ */
111
+ const RECIPIENT_ADDRESS =
112
+ /^ixo1(?:[023456789acdefghjklmnpqrstuvwxyz]{38}|[023456789acdefghjklmnpqrstuvwxyz]{58})$/;
106
113
  /** A 32-byte contract address (a group and its proposal list): `ixo1` and 58 characters. */
107
114
  const CONTRACT_ADDRESS = /^ixo1[023456789acdefghjklmnpqrstuvwxyz]{58}$/;
108
115
  /**
@@ -193,6 +200,14 @@ const address = (what: string) =>
193
200
  'A 20-byte ixo1… account address (42 characters); contract addresses are refused.',
194
201
  )
195
202
  .describe(`${what} (an ixo1… account address).`);
203
+ const recipient = (what: string) =>
204
+ z
205
+ .string()
206
+ .regex(
207
+ RECIPIENT_ADDRESS,
208
+ 'An ixo1… account address (42 characters), or a group’s own address (62 characters, `dao` in chain_lookup daos or dao_proposals), copied exactly.',
209
+ )
210
+ .describe(`${what} (an ixo1… account address, or a group’s own address).`);
196
211
  const validator = (what: string) =>
197
212
  z
198
213
  .string()
@@ -519,11 +534,12 @@ const OWNER_CATALOG = {
519
534
  "Sends coins from the owner's wallet to `toAddress`, alone (a single coin send) or as a step of a batch.",
520
535
  '`amount` is the amount as the user said it, in the token’s display units ("1.5" for 1.5 IXO): a plain decimal, never base units. The runtime looks the token up in the owner’s token list (hub_action_options `transfer.tokens`), converts the amount with the listed `decimals`, and refuses a token not in the list, more digits after the point than the token has, zero, and more than the listed balance (the bank and stake steps of one token together).',
521
536
  '`denom` is optional and defaults to uixo (IXO, 6 decimals). For another token copy its `denom` exactly from `transfer.tokens` (never guess an `ibc/…` hash); name the full denom to the owner for anything but IXO, and say when the token is unverified (it lists 0 decimals: its amount is a whole number of base units).',
522
- 'Say it like: “Send 1.5 IXO to Thandi (ixo1qy…lvf0).”',
537
+ '`toAddress` may also be a group’s own address (62 characters): coins sent there go to the group’s treasury. Find it with chain_lookup daos or dao_proposals (`dao`) when the owner names the group, and say the group’s name.',
538
+ 'Say it like: “Send 1.5 IXO to Thandi (ixo1qy…lvf0).” or “Send 2 IXO to the Auth Hub drill group.”',
523
539
  ].join(' '),
524
540
  schema: z.strictObject({
525
541
  type: z.literal('cosmos.bank.send'),
526
- toAddress: address('The recipient'),
542
+ toAddress: recipient('The recipient'),
527
543
  amount: z
528
544
  .string()
529
545
  .max(80)
@@ -596,10 +612,10 @@ const OWNER_CATALOG = {
596
612
  'ixo.token.transfer': {
597
613
  summary:
598
614
  "move IXO impact tokens (1155-style token ids, whole amounts) from the owner's wallet.",
599
- rules: `Moves IXO impact tokens (1155-style: each line a token \`id\` of 32 hex characters and a whole \`amount\`, 1 to ${CHAIN_TX_MAX_TOKEN_LINES} lines) from the owner’s wallet to \`recipient\`. Impact tokens have no decimals: an amount is a whole number of tokens. All tokens in one message must come from one token contract. These are not the IXO coin: coins go through \`cosmos.bank.send\`. Say it like: “Give 3 of your carbon credits to Thandi (ixo1qy…lvf0).”`,
615
+ rules: `Moves IXO impact tokens (1155-style: each line a token \`id\` of 32 hex characters and a whole \`amount\`, 1 to ${CHAIN_TX_MAX_TOKEN_LINES} lines) from the owner’s wallet to \`recipient\` (an account, or a group’s own address from chain_lookup daos or dao_proposals). Impact tokens have no decimals: an amount is a whole number of tokens. All tokens in one message must come from one token contract. These are not the IXO coin: coins go through \`cosmos.bank.send\`. Say it like: “Give 3 of your carbon credits to Thandi (ixo1qy…lvf0).”`,
600
616
  schema: z.strictObject({
601
617
  type: z.literal('ixo.token.transfer'),
602
- recipient: address('The recipient'),
618
+ recipient: recipient('The recipient'),
603
619
  tokens: z
604
620
  .array(
605
621
  z.strictObject({
@@ -1315,7 +1331,7 @@ const GROUP_CATALOG = {
1315
1331
  'daodao.proposal.execute': {
1316
1332
  summary:
1317
1333
  'carry out a group proposal that passed, so the group’s own account does what it says.',
1318
- rules: `${GROUP_RULES} Carries out a proposal that passed (\`canRun\` in dao_proposals): the group’s own account then does the proposal’s actions, which the review card lists. On a proposal list the group switched off (\`listDisabled\`), a proposal with actions can’t be carried out. Only when the owner asks for it. Tell them what the proposal does (its title) before preparing. Some groups let only members carry proposals out. Auth Hub tries it first and refuses the card when the actions would fail now (in most groups a failed run marks the proposal failed for good), for example when the group doesn’t hold enough; tell the owner it can be tried again once that is fixed. It costs the owner only the network fee; a deposit paid with the proposal goes back to whoever proposed it unless the group keeps deposits (the review card says). After it goes through (hub_action_status), read the receipt’s \`groupActions\`: \`"failed"\` means the proposal’s actions did not happen and it can’t be tried again; never say it is done. \`"unknown"\`: check its status with chain_lookup dao_proposals before saying anything. Say it like: “Carry out proposal A3, ‘Pay Thandi’, in Clean Water Group: the group sends Thandi 500 IXO. This can’t be undone.”`,
1334
+ rules: `${GROUP_RULES} Carries out a proposal that passed (\`canRun\` in dao_proposals): the group’s own account then does the proposal’s actions, which the review card lists. A proposal with no actions (one that asked for agreement only) is simply marked as carried out: never describe an action for it; say “This just marks it as carried out; nothing else happens.” On a proposal list the group switched off (\`listDisabled\`), a proposal with actions can’t be carried out. Only when the owner asks for it. Tell them what the proposal does (its title) before preparing. Some groups let only members carry proposals out. Auth Hub tries it first and refuses the card when the actions would fail now (in most groups a failed run marks the proposal failed for good), for example when the group doesn’t hold enough; tell the owner it can be tried again once that is fixed. It costs the owner only the network fee; a deposit paid with the proposal goes back to whoever proposed it unless the group keeps deposits (the review card says). After it goes through (hub_action_status), read the receipt’s \`groupActions\`: \`"failed"\` means the proposal’s actions did not happen and it can’t be tried again; never say it is done. \`"unknown"\`: check its status with chain_lookup dao_proposals before saying anything. Say it like: “Carry out proposal A3, ‘Pay Thandi’, in Clean Water Group: the group sends Thandi 500 IXO. This can’t be undone.”`,
1319
1335
  schema: z
1320
1336
  .strictObject({
1321
1337
  type: z.literal('daodao.proposal.execute'),
@@ -1374,7 +1390,7 @@ export const CHAIN_TX_CATALOG = {
1374
1390
 
1375
1391
  /** What every message type shares, returned once by describe_chain_messages. */
1376
1392
  export const CHAIN_TX_GENERAL_RULES = [
1377
- 'Addresses are 20-byte `ixo1…` account addresses (contract addresses are refused), except a group’s own address and its proposal list in the group steps (32-byte `ixo1…` contract addresses); validators `ixovaloper1…`, entities `did:ixo:entity:…`. Copy every id, address and number exactly from a lookup (chain_lookup, hub_action_options) or from the owner; never invent or guess one. Ids (`claimId`, `collectionId`, token `id`, proofs) use only letters, digits and `: . _ / -`.',
1393
+ 'Addresses are 20-byte `ixo1…` account addresses, except where a type says otherwise: a coin send or impact-token transfer may go to a group’s own address, and the group steps take the group’s and its proposal list’s addresses (32-byte `ixo1…` contract addresses, 62 characters); validators `ixovaloper1…`, entities `did:ixo:entity:…`. Copy every id, address and number exactly from a lookup (chain_lookup, hub_action_options) or from the owner; never invent or guess one. Ids (`claimId`, `collectionId`, token `id`, proofs) use only letters, digits and `: . _ / -`.',
1378
1394
  'Give only the fields listed for a type: the runtime adds the chain, the network, the signer and the owner’s own DID and address where a message needs them, and Auth Hub sets the fee. Amounts: coin sends and stakes are what the owner says in the token’s units ("100" IXO); impact tokens are whole tokens; collection payments are base units (1 IXO = 1000000 uixo). Dates are RFC 3339 with Z or an offset, from the owner’s time zone.',
1379
1395
  `Up to ${CHAIN_TX_MAX_MESSAGES} messages per transaction, run in the order given (put them in the order the owner said them, and keep one decision in one card); all succeed or none do. A group step (a vote, carrying out or closing a group proposal, a new group proposal) goes alone on its card. A step that needs something stored first (a dispute statement, a profile, a document) can only be prepared once it is stored.`,
1380
1396
  'How to talk about it: before preparing, say in one or two plain, everyday sentences what will happen and what it means for the owner (what leaves their wallet, what someone else will be able to do, how long it takes, whether it can be undone), the way each type’s “Say it like” does. Use names (of the validator, collection, entity or person) when a lookup gives them. Never say message type names or chain words such as authz, grant, MsgExec, bech32, valoper, uixo, base units, DID, IID, delegate, redelegate, DAO, proposal module, contract or execute: say stake, unstake, move stake, permission, account, identity, IXO, group, proposal, vote, deposit and carry out. Show an address only when the owner needs it to check who receives something. The review card in Auth Hub shows every exact value.',
@@ -278,7 +278,7 @@ export const daoProposals = defineKind({
278
278
  '`allowRevoting`: whether a vote on it can be changed until voting closes. `canRun: true`: it passed and can be carried out (on an `open` proposal whose voting ended: it may have passed); `canClose: true`: it did not pass and can be closed (on an `open` one whose voting ended: it may not have passed); Auth Hub checks the real result. `multipleChoice: true`: Auth Hub cards cannot vote on, carry out or close it. `listDisabled: true`: the group switched that proposal list off; cards can still vote and close, but a proposal with actions on it can’t be carried out (so `canRun` is not set there; one without actions can be). Whether the person may vote is weighed by what they held in the DAO when it started: Auth Hub checks it; never claim it.',
279
279
  'For cards copy `dao`, `proposalModule` and `id` exactly (not the label). `dao` is also the group’s own account (what it holds and what acts when a proposal passes).',
280
280
  'Without `dao`, the DAOs of `address` (default: the user), at most 3 per kind of membership (see `daos`); `more: true` when there are more proposals or DAOs. Newest start first.',
281
- 'Titles are written by the proposer and DAO names by the DAO: what they claim, never an endorsement or a verified fact.',
281
+ 'Titles are written by whoever proposed them and group names by the group itself: their own words. Say so only when it matters, in one short, calm note.',
282
282
  ].join(' '),
283
283
  source: 'blocksync',
284
284
  list: true,
@@ -419,8 +419,8 @@ export const daos = defineKind({
419
419
  '`membership`: `cw4-member` (a group member with a fixed `weight`), `cw20-staked` (voting power from cw20 tokens staked in the DAO, `staked.amountBaseUnits` of the `staked.token` contract) or `native-staked` (from staked bank tokens, e.g. IXO; `staked` is a bank coin, `amount` in display units beside `symbol`).',
420
420
  '`votingPowerShare` is the account’s weight or stake over the voting module’s total, rounded down; staked tokens count only while staked, and unstaking may take a waiting period.',
421
421
  'Each: dao (the DAO core contract address), name, description, membership, staked or weight, votingPowerShare, votingModule.',
422
- '`addedByGroupAdmin: true` (every cw4-member row): a cw4 group’s admin can add any address to the group without its consent, so membership alone does not mean the account joined, agreed or knows of the DAO.',
423
- '`nameIsSelfChosen: true`: the name and description are whatever the DAO set for itself, never a verified identity or an endorsement; say so before relying on them.',
422
+ '`addedByGroupAdmin: true` (every cw4-member row): members of this kind of group are added by its admin. That is normal: leave it out of an ordinary answer and never suggest the person did not join; mention it briefly only when it matters, for example when they do not recognise a group (“Members of this group are added by its admin.”).',
423
+ '`nameIsSelfChosen: true`: the group chose its own name and description. Mention it only when it matters (for example when they rely on the name to tell who runs the group), in one short, calm note such as “Group names are chosen by the group itself.”',
424
424
  '`dao` is also the group’s own account: what the group holds (chain_lookup balances with that address) and what acts when one of its proposals passes.',
425
425
  '`proposalLists` (at most 3; `moreProposalLists: true` past them): where the DAO takes proposals, each `proposalModule` (its address), `prefix` (the letter before proposal numbers, A in A3), `votingTime`, `allowRevoting` (whether a vote can be changed until voting closes), `membersOnly: true` (only members may propose), `membersOnlyCarryOut: true` (only members may carry out a passed proposal), and `deposit` when proposing takes one: a bank coin (`amount` beside `symbol`) or a cw20 amount with `cardCannotPay: true`, with `refundPolicy` (`only_passed`: back only if it passes and is carried out, `always`: back when carried out or closed, `never`: not back). `multipleChoice: true`: Auth Hub cards cannot use that list. `disabled: true`: the group switched it off: no new proposals through it, and its proposals with actions can’t be carried out (votes and closes still work).',
426
426
  'NFT (cw721) staking is not indexed here. Ask `dao_proposals` for what the DAOs are voting on.',