@spirobel/monero-wallet-api 0.6.12 → 0.6.16

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.
@@ -26,17 +26,24 @@ function make(payload) {
26
26
  const base = `${TOOL_MAGIC_STRING}001_amount_${payload.amount}_address_${payload.address}`;
27
27
  return payload.no_check ? `${base}_no_check` : base;
28
28
  }
29
+ // base58, 95 chars for a standard address and 106 for an integrated one.
30
+ const MONERO_ADDRESS_SHAPE = /^[1-9A-HJ-NP-Za-km-z]{95}$|^[1-9A-HJ-NP-Za-km-z]{106}$/;
29
31
  async function validate_check(invo) {
30
32
  const tool = invo.tool;
31
33
  if (tool.tool_id !== "001")
32
34
  return "unverified";
33
35
  if (tool.payload.no_check)
34
36
  return "unverified";
37
+ // the address goes into the path. a real address is base58 and 95 or 106 chars.
38
+ // anything else could rewrite the request path, so it never reaches the fetch.
39
+ if (!MONERO_ADDRESS_SHAPE.test(tool.payload.address))
40
+ return "invalid";
35
41
  const link = invo[invo.found_in];
36
42
  const invo_link = new URL(link);
37
43
  const checkUrl = `${invo_link.origin}/monerochan001/${tool.payload.address}`;
38
44
  try {
39
- const result = (await (await fetch(checkUrl)).json());
45
+ // no redirects: a shop must not bounce the request to another host.
46
+ const result = (await (await fetch(checkUrl, { redirect: "error" })).json());
40
47
  if (result &&
41
48
  typeof result === "object" &&
42
49
  "valid_address" in result &&
@@ -1,22 +1,8 @@
1
- type ExtensionRuntime = {
2
- sendMessage: (msg: unknown) => Promise<unknown>;
3
- connect: (info: {
4
- name: string;
5
- }) => {
6
- onMessage: {
7
- addListener: (cb: (msg: unknown) => void) => void;
8
- };
9
- };
10
- };
11
- type ExtensionBrowser = {
12
- runtime: ExtensionRuntime;
13
- };
1
+ import type { ExtensionBrowser } from "./globals";
14
2
  declare global {
15
3
  var browser: ExtensionBrowser;
16
4
  var chrome: ExtensionBrowser;
17
5
  }
18
6
  export declare function interceptToolLinkCallback(e: Event): void;
19
7
  export declare function deliverToolUrl(url: string): boolean;
20
- export declare function DefaultConfirmationCallback(loc_href: string): boolean;
21
- export declare function installToolLinkEventInterception(confirmationcallback?: (loc_href: string) => boolean): void;
22
- export {};
8
+ export declare function installToolLinkEventInterception(): void;
@@ -71,11 +71,8 @@ export function deliverToolUrl(url) {
71
71
  deliver(invo);
72
72
  return true;
73
73
  }
74
- export function DefaultConfirmationCallback(loc_href) {
75
- return window.confirm("Did you just follow a monerochan tool link?");
76
- }
77
- export function installToolLinkEventInterception(confirmationcallback = DefaultConfirmationCallback) {
74
+ export function installToolLinkEventInterception() {
75
+ // click only. keyboard activation (enter) dispatches a click, a touch tap does too.
76
+ // a keydown on a focused link is not an activation, so it must not deliver.
78
77
  document.addEventListener("click", interceptToolLinkCallback, true);
79
- document.addEventListener("touchend", interceptToolLinkCallback, true);
80
- document.addEventListener("keydown", interceptToolLinkCallback, true);
81
78
  }
@@ -120,6 +120,7 @@ export type ExtensionRuntime = {
120
120
  connect: (info: {
121
121
  name: string;
122
122
  }) => ExtensionPort;
123
+ getURL: (path: string) => string;
123
124
  onMessage: {
124
125
  addListener: (cb: (msg: unknown, sender: unknown) => void) => void;
125
126
  };
@@ -127,6 +128,21 @@ export type ExtensionRuntime = {
127
128
  addListener: (cb: (port: ExtensionPort) => void) => void;
128
129
  };
129
130
  };
131
+ export type ExtensionBrowser = {
132
+ runtime: ExtensionRuntime;
133
+ tabs: {
134
+ query: (query: {
135
+ url?: string;
136
+ }) => Promise<{
137
+ id?: number;
138
+ }[]>;
139
+ remove: (ids: number[]) => Promise<void>;
140
+ update: (id: number, update: {
141
+ url?: string;
142
+ active?: boolean;
143
+ }) => Promise<unknown>;
144
+ };
145
+ };
130
146
  export declare function getExtensionRuntime(): ExtensionRuntime | null;
131
147
  export declare function sendToBackground<K extends ExtensionMessageKind>(kind: K, payload: ExtensionMessageByKind[K]): Promise<unknown>;
132
148
  export type PortMessageByKind = {
@@ -2,6 +2,7 @@ import { TOOL_MAGIC_STRING, type ToolArm, type ToolInvocationValidity, type Tool
2
2
  export { TOOL_MAGIC_STRING, type ToolInvocationValidity };
3
3
  export type { ToolArm, ToolContentZone, ToolCounterpartyZone, ToolInvocationForValidate, ToolWorkerZone, } from "./globals";
4
4
  export type { ToolUiCopy, ToolNotice, ToolPermission, } from "./globals";
5
+ export type { ExtensionRuntime, ExtensionBrowser } from "./globals";
5
6
  export declare const tools: {
6
7
  readonly "001": ToolArm<import("./calls/001").SendTransactionToolPayload, {
7
8
  validate_route: (deps: {
@@ -14,6 +15,7 @@ export declare const tools: {
14
15
  };
15
16
  export type ToolId = keyof typeof tools;
16
17
  export declare const TOOL_IDS: ToolId[];
18
+ export declare const TOOL_IDS_WITHOUT_DIRECT_OPEN: ToolId[];
17
19
  export declare function getToolUiByPermissions(permissions: ToolPermission[]): ToolUiCopy | null;
18
20
  type PayloadOf<A> = A extends ToolArm<infer P, infer _E> ? P : never;
19
21
  export type MoneroTool = {
@@ -10,6 +10,9 @@ export const tools = {
10
10
  "002": tool002,
11
11
  };
12
12
  export const TOOL_IDS = Object.keys(tools);
13
+ // tool calls that cannot be started from a tool link. the content script port
14
+ // carries the rest of the exchange, so the page that made the call has to stay.
15
+ export const TOOL_IDS_WITHOUT_DIRECT_OPEN = ["002"];
13
16
  export function getToolUiByPermissions(permissions) {
14
17
  const want = new Set(permissions);
15
18
  for (const id of Object.keys(tools)) {
@@ -25,6 +28,34 @@ function isToolId(id) {
25
28
  return id in tools;
26
29
  }
27
30
  export function parseToolLink(link) {
31
+ // the browser can wrap a pasted monero: uri in the search url it built, escaped or plain
32
+ const lower = link.toLowerCase();
33
+ const plain_at = lower.indexOf("monero:");
34
+ const escaped_at = lower.indexOf("monero%3a");
35
+ const legacy_at = plain_at !== -1 ? plain_at : escaped_at;
36
+ if (legacy_at !== -1) {
37
+ try {
38
+ // an escaped uri sits inside one parameter, so the next raw & ends it
39
+ //...?q=monero%3AADDR&source=desktop
40
+ // the pasted uri, ends at &
41
+ const tail = link.slice(legacy_at);
42
+ const uri = new URL(decodeURIComponent(plain_at !== -1 ? tail : tail.split("&")[0] ?? ""));
43
+ const address = uri.pathname;
44
+ const amount = uri.searchParams.get("tx_amount") ?? "";
45
+ if (!/^([1-9A-HJ-NP-Za-km-z]{95}|[1-9A-HJ-NP-Za-km-z]{106})$/.test(address))
46
+ return null;
47
+ if (amount && !/^\d+(?:\.\d{1,12})?$/.test(amount))
48
+ return null;
49
+ // separate payment ids are not supported. do not drop one or choose between amounts.
50
+ if (uri.searchParams.has("tx_payment_id") || uri.searchParams.getAll("tx_amount").length > 1)
51
+ return null;
52
+ return { tool_id: "001", payload: { address, amount, no_check: true } };
53
+ }
54
+ catch {
55
+ // a broken monero uri is not a tool call. do not fall through to the marker scan.
56
+ return null;
57
+ }
58
+ }
28
59
  const magic_str_index = link.lastIndexOf(TOOL_MAGIC_STRING);
29
60
  if (magic_str_index === -1)
30
61
  return null;