@ai-ecoverse/wasi-ntfy 0.0.0 → 2.32.0-2

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/bin/ntfy.wasm ADDED
Binary file
@@ -0,0 +1,172 @@
1
+ export const PROBE = 0x6e746679
2
+ const READ_MAX = 65536
3
+ const STDIO = ['inherit', 'pipe']
4
+ const encoder = new TextEncoder()
5
+ const decoder = new TextDecoder()
6
+
7
+ export function fromBase64 (text) {
8
+ if (!text) return new Uint8Array(0)
9
+ if (typeof Uint8Array.fromBase64 === 'function') return Uint8Array.fromBase64(text)
10
+ return Uint8Array.from(atob(text), c => c.charCodeAt(0))
11
+ }
12
+
13
+ export function toBase64 (bytes) {
14
+ if (typeof bytes?.toBase64 === 'function') return bytes.toBase64()
15
+ let binary = ''
16
+ for (let at = 0; at < bytes.length; at += 32768) binary += String.fromCharCode(...bytes.subarray(at, at + 32768))
17
+ return btoa(binary)
18
+ }
19
+
20
+ export function envObject (env) {
21
+ const out = {}
22
+ for (const entry of env ?? []) {
23
+ const at = entry.indexOf('=')
24
+ if (at > 0) out[entry.slice(0, at)] = entry.slice(at + 1)
25
+ }
26
+ return out
27
+ }
28
+
29
+ export function responseHeaders (head) {
30
+ const pairs = (head.headers ?? []).map(([name, value]) => [String(name), String(value)])
31
+ if (head.decoded === false) return pairs
32
+ const encoding = pairs.find(([name]) => name.toLowerCase() === 'content-encoding')?.[1]
33
+ const decoded = encoding?.split(',').every(value => ['gzip', 'deflate', 'br'].includes(value.trim().toLowerCase()))
34
+ if (!decoded) return pairs
35
+ return pairs.filter(([name]) => !['content-encoding', 'content-length'].includes(name.toLowerCase()))
36
+ }
37
+
38
+ const concat = parts => {
39
+ const out = new Uint8Array(parts.reduce((total, part) => total + part.length, 0))
40
+ let at = 0
41
+ for (const part of parts) {
42
+ out.set(part, at)
43
+ at += part.length
44
+ }
45
+ return out
46
+ }
47
+
48
+ const reason = error => error?.message ?? String(error)
49
+ const mode = value => (STDIO.includes(value) ? value : 'pipe')
50
+
51
+ export function createHost (ctx) {
52
+ function run ({ argv, env, dir, input, stdin, stdout, stderr }) {
53
+ if (!Array.isArray(argv) || argv.length === 0 || argv.some(arg => typeof arg !== 'string')) {
54
+ throw new Error('run needs an argv of strings')
55
+ }
56
+ const stdio = { stdin: mode(stdin), stdout: mode(stdout), stderr: mode(stderr) }
57
+ let child
58
+ try {
59
+ child = ctx.spawn({ argv, env: envObject(env), cwd: dir || ctx.cwd(), ...stdio })
60
+ } catch (error) {
61
+ const code = error?.code ?? reason(error)
62
+ throw new Error(code === 'ENOENT' ? `exec: "${argv[0]}": executable file not found in $PATH` : `exec: "${argv[0]}": ${code}`)
63
+ }
64
+ const pending = new Map()
65
+ const output = { stdout: [], stderr: [] }
66
+ let body = stdio.stdin === 'pipe' ? fromBase64(input) : new Uint8Array(0)
67
+ const read = stream => pending.set(ctx.async.submit({ op: 'fd-read', fd: child[stream], max: READ_MAX }), stream)
68
+ const write = () => {
69
+ if (stdio.stdin !== 'pipe') return
70
+ if (body.length === 0) {
71
+ ctx.syscall({ op: 'fd-close', fd: child.stdin })
72
+ return
73
+ }
74
+ pending.set(ctx.async.submit({ op: 'fd-write', fd: child.stdin, body }), 'stdin')
75
+ }
76
+ write()
77
+ if (stdio.stdout === 'pipe') read('stdout')
78
+ if (stdio.stderr === 'pipe') read('stderr')
79
+ while (pending.size > 0) {
80
+ const id = ctx.async.wait()
81
+ const stream = pending.get(id)
82
+ if (stream === undefined) continue
83
+ pending.delete(id)
84
+ const taken = ctx.async.take(id)
85
+ if (taken?.error !== undefined) {
86
+ if (stream === 'stdin') {
87
+ body = new Uint8Array(0)
88
+ write()
89
+ continue
90
+ }
91
+ ctx.syscall({ op: 'fd-close', fd: child[stream] })
92
+ continue
93
+ }
94
+ if (stream === 'stdin') {
95
+ const written = Number(taken?.value)
96
+ body = written > 0 ? body.subarray(written) : new Uint8Array(0)
97
+ write()
98
+ continue
99
+ }
100
+ const bytes = taken?.value
101
+ if (!(bytes instanceof Uint8Array) || bytes.length === 0) {
102
+ ctx.syscall({ op: 'fd-close', fd: child[stream] })
103
+ continue
104
+ }
105
+ output[stream].push(bytes)
106
+ read(stream)
107
+ }
108
+ const { status } = ctx.wait(child.pid)
109
+ return { status, stdout: toBase64(concat(output.stdout)), stderr: toBase64(concat(output.stderr)) }
110
+ }
111
+
112
+ function http ({ method, url, headers, body }) {
113
+ let target
114
+ try {
115
+ target = new URL(url)
116
+ } catch {
117
+ throw new Error(`invalid URL ${url}`)
118
+ }
119
+ if (target.protocol !== 'http:' && target.protocol !== 'https:') throw new Error(`unsupported protocol ${target.protocol}`)
120
+ const bytes = fromBase64(body)
121
+ let head
122
+ try {
123
+ head = ctx.syscall({ op: 'net-request', url: target.href, method: method || 'GET', headers: headers ?? [], ...(bytes.length ? { body: bytes } : {}) })
124
+ } catch (error) {
125
+ throw new Error(`could not reach ${target.host} (${error?.code ?? reason(error)})`)
126
+ }
127
+ return { handle: head.handle, status: head.status, headers: responseHeaders(head) }
128
+ }
129
+
130
+ function httpRead ({ handle }) {
131
+ const bytes = ctx.syscall({ op: 'net-read', handle, max: READ_MAX })
132
+ return toBase64(bytes instanceof Uint8Array ? bytes : new Uint8Array(0))
133
+ }
134
+
135
+ function httpClose ({ handle }) {
136
+ ctx.syscall({ op: 'net-close', handle })
137
+ return null
138
+ }
139
+
140
+ const ops = { run, http, 'http.read': httpRead, 'http.close': httpClose }
141
+ return request => {
142
+ const op = ops[request?.op]
143
+ if (!op) throw new Error(`unknown ntfy_host operation ${request?.op}`)
144
+ return op(request)
145
+ }
146
+ }
147
+
148
+ export function createImports (ctx) {
149
+ const handle = createHost(ctx)
150
+ let result = new Uint8Array(0)
151
+ const bytes = () => new Uint8Array(ctx.memory().buffer)
152
+ return {
153
+ ntfy_host: {
154
+ probe: () => PROBE,
155
+ call: (pointer, size) => {
156
+ let response
157
+ try {
158
+ const request = JSON.parse(decoder.decode(bytes().slice(pointer >>> 0, (pointer >>> 0) + (size >>> 0))))
159
+ response = { ok: true, value: handle(request) ?? null }
160
+ } catch (error) {
161
+ response = { ok: false, error: reason(error) }
162
+ }
163
+ result = encoder.encode(JSON.stringify(response))
164
+ return result.length
165
+ },
166
+ take: pointer => {
167
+ bytes().set(result, pointer >>> 0)
168
+ result = new Uint8Array(0)
169
+ },
170
+ },
171
+ }
172
+ }
package/package.json CHANGED
@@ -1,10 +1,53 @@
1
1
  {
2
2
  "name": "@ai-ecoverse/wasi-ntfy",
3
- "version": "0.0.0",
4
- "description": "ntfy client as a WASI command for slicc (ai-ecoverse/homescoop)",
5
- "license": "Apache-2.0",
3
+ "version": "2.32.0-2",
4
+ "description": "ntfy 2.32.0 client (ntfy pub, ntfy sub) for slicc WASI preview1 (command ntfy); ntfy.wasm built by homescoop from ntfy v2.32.0 with documented patches",
5
+ "license": "Apache-2.0 OR GPL-2.0-only",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "git+https://github.com/ai-ecoverse/homescoop.git"
8
+ "url": "git+https://github.com/ai-ecoverse/homescoop.git",
9
+ "directory": "packages/wasi-ntfy/package"
10
+ },
11
+ "homepage": "https://github.com/ai-ecoverse/homescoop/tree/main/packages/wasi-ntfy",
12
+ "keywords": [
13
+ "wasm",
14
+ "wasi",
15
+ "slicc",
16
+ "homescoop",
17
+ "ntfy",
18
+ "webhook",
19
+ "notifications"
20
+ ],
21
+ "files": [
22
+ "README.md",
23
+ "LICENSE",
24
+ "LICENSE.GPLv2",
25
+ "THIRD-PARTY-NOTICES.md",
26
+ "bin",
27
+ "host"
28
+ ],
29
+ "engines": {
30
+ "slicc-kernel": ">=1.125.4"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "homescoop": {
36
+ "recipe": "wasi-ntfy",
37
+ "upstream": "2.32.0",
38
+ "source": "binwiederhier/ntfy@9d0b6d933d6a6ddc5265bfecadaec1fb1538d89b",
39
+ "patches": [
40
+ "0001-wasip1-slicc-host.patch",
41
+ "0002-gateway-contract.patch"
42
+ ]
43
+ },
44
+ "slicc": {
45
+ "abi": "wasi",
46
+ "commands": {
47
+ "ntfy": {
48
+ "wasm": "bin/ntfy.wasm",
49
+ "imports": "host/ntfy-host.mjs"
50
+ }
51
+ }
9
52
  }
10
53
  }