@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/LICENSE +201 -0
- package/LICENSE.GPLv2 +340 -0
- package/README.md +125 -4
- package/THIRD-PARTY-NOTICES.md +849 -0
- package/bin/ntfy.wasm +0 -0
- package/host/ntfy-host.mjs +172 -0
- package/package.json +47 -4
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": "
|
|
4
|
-
"description": "ntfy client
|
|
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
|
}
|