@johannc-dev/zyrk 0.0.0-stage → 0.2.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/README.md +79 -2
- package/dist/build.d.ts +48 -0
- package/dist/build.js +338 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +295 -0
- package/dist/client.d.ts +208 -0
- package/dist/client.js +416 -0
- package/dist/contracts.d.ts +312 -0
- package/dist/contracts.js +154 -0
- package/dist/errors.d.ts +91 -0
- package/dist/errors.js +58 -0
- package/dist/export.d.ts +87 -0
- package/dist/export.js +338 -0
- package/dist/globals.d.ts +59 -0
- package/dist/hourfile.d.ts +57 -0
- package/dist/hourfile.js +338 -0
- package/dist/limits.d.ts +91 -0
- package/dist/limits.js +54 -0
- package/dist/local-data.d.ts +106 -0
- package/dist/local-data.js +345 -0
- package/dist/local-replay.d.ts +40 -0
- package/dist/local-replay.js +342 -0
- package/dist/local-storage.d.ts +76 -0
- package/dist/local-storage.js +499 -0
- package/dist/local.d.ts +235 -0
- package/dist/local.js +1133 -0
- package/dist/sandbox-bootstrap.js +519 -0
- package/dist/sdk.d.ts +1129 -0
- package/dist/sdk.js +208 -0
- package/dist/stream.d.ts +36 -0
- package/dist/stream.js +69 -0
- package/dist/webhook.d.ts +43 -0
- package/dist/webhook.js +54 -0
- package/package.json +56 -4
package/dist/sdk.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
export const Side = { buy: 0, sell: 1, event: 2 };
|
|
2
|
+
export const Protocol = { pumpfun: 0, pumpswap: 1 };
|
|
3
|
+
export const OrderFlag = {
|
|
4
|
+
known: 1,
|
|
5
|
+
bySignature: 2,
|
|
6
|
+
ambiguous: 4,
|
|
7
|
+
amountDiffers: 8,
|
|
8
|
+
unknownNoMatch: 16,
|
|
9
|
+
unknownNotCovered: 32,
|
|
10
|
+
unknownNoKey: 64,
|
|
11
|
+
notFinalized: 128,
|
|
12
|
+
};
|
|
13
|
+
export const NO_TX_INDEX = 0xffff_ffff;
|
|
14
|
+
export const NO_EVENT_INDEX = 0xffff;
|
|
15
|
+
export const NO_TX_ID = 0xffff_ffff;
|
|
16
|
+
export const TxFlag = {
|
|
17
|
+
bySignature: 1,
|
|
18
|
+
byChain: 2,
|
|
19
|
+
chainIndex: 4,
|
|
20
|
+
providerIndex: 8,
|
|
21
|
+
unknown: 16,
|
|
22
|
+
};
|
|
23
|
+
export const NO_ADDRESS = -1;
|
|
24
|
+
export const HOUR_MS = 3_600_000;
|
|
25
|
+
export const SOL_MINT = "So11111111111111111111111111111111111111112";
|
|
26
|
+
export const MarketEvent = {
|
|
27
|
+
none: 0,
|
|
28
|
+
migrate: 1,
|
|
29
|
+
createPool: 2,
|
|
30
|
+
add: 3,
|
|
31
|
+
remove: 4,
|
|
32
|
+
create: 5,
|
|
33
|
+
feeHolders: 6,
|
|
34
|
+
feeShareholders: 7,
|
|
35
|
+
feeCreator: 8,
|
|
36
|
+
feeSocial: 9,
|
|
37
|
+
feeOther: 10,
|
|
38
|
+
};
|
|
39
|
+
export const MarketFlag = {
|
|
40
|
+
reserves: 1,
|
|
41
|
+
virtualReserves: 2,
|
|
42
|
+
multiOpRow: 4,
|
|
43
|
+
chainBreak: 8,
|
|
44
|
+
chainUnchecked: 16,
|
|
45
|
+
creatorInitialBuy: 32,
|
|
46
|
+
cashback: 64,
|
|
47
|
+
mayhem: 128,
|
|
48
|
+
feeWithoutToken: 256,
|
|
49
|
+
invertedPool: 512,
|
|
50
|
+
};
|
|
51
|
+
export function isFinal(result) {
|
|
52
|
+
return result.status !== "queued" && result.status !== "running";
|
|
53
|
+
}
|
|
54
|
+
export function txFlagNames(flags) {
|
|
55
|
+
return Object.keys(TxFlag).filter((k) => (flags & TxFlag[k]) !== 0);
|
|
56
|
+
}
|
|
57
|
+
export function sameTransaction(a, i, b, j) {
|
|
58
|
+
if (!a.tx || !b.tx)
|
|
59
|
+
throw new RangeError("sameTransaction needs batches read withTx: true");
|
|
60
|
+
const x = a.tx.id[i] ?? NO_TX_ID;
|
|
61
|
+
return a.hour === b.hour && x !== NO_TX_ID && x === b.tx.id[j];
|
|
62
|
+
}
|
|
63
|
+
export function deliveryNow(delivery) {
|
|
64
|
+
return delivery.replay?.clockMs ?? Date.now();
|
|
65
|
+
}
|
|
66
|
+
export function isDelivery(args) {
|
|
67
|
+
return isRecord(args) && args.kind === "events" && Array.isArray(args.events);
|
|
68
|
+
}
|
|
69
|
+
export function isCompletion(args) {
|
|
70
|
+
return (isRecord(args) &&
|
|
71
|
+
args.kind === "completion" &&
|
|
72
|
+
typeof args.executionId === "string");
|
|
73
|
+
}
|
|
74
|
+
export const SDK_MARKER = "__zyrkSdk";
|
|
75
|
+
export const SDK_VERSION = "0.2";
|
|
76
|
+
export function defineProgram(program) {
|
|
77
|
+
Object.defineProperty(program, SDK_MARKER, {
|
|
78
|
+
value: SDK_VERSION,
|
|
79
|
+
enumerable: false,
|
|
80
|
+
});
|
|
81
|
+
return program;
|
|
82
|
+
}
|
|
83
|
+
export function tradeId(batch, i) {
|
|
84
|
+
return `${batch.hour}:${batch.line[i]}:${batch.op[i]}`;
|
|
85
|
+
}
|
|
86
|
+
export function parseTradeId(id) {
|
|
87
|
+
const match = /^(\d+):(\d+):(\d+)$/.exec(id);
|
|
88
|
+
if (!match)
|
|
89
|
+
throw new RangeError(`Invalid trade id: ${id}`);
|
|
90
|
+
return {
|
|
91
|
+
hour: Number(match[1]),
|
|
92
|
+
line: Number(match[2]),
|
|
93
|
+
op: Number(match[3]),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
export function compareTradeIds(a, b) {
|
|
97
|
+
const x = parseTradeId(a), y = parseTradeId(b);
|
|
98
|
+
return x.hour - y.hour || x.line - y.line || x.op - y.op;
|
|
99
|
+
}
|
|
100
|
+
export function operationTradeId(operation) {
|
|
101
|
+
const id = operation.id;
|
|
102
|
+
if (typeof id === "string" && /^\d+:\d+:\d+$/.test(id))
|
|
103
|
+
return id;
|
|
104
|
+
return operation.order?.ord ?? null;
|
|
105
|
+
}
|
|
106
|
+
export function priceFactor(percent) {
|
|
107
|
+
const ratio = percent >= 0 ? 1 + percent / 100 : 1 / (1 + percent / 100);
|
|
108
|
+
const factor = Math.floor(ratio * 10_000 + 1e-9) / 10_000;
|
|
109
|
+
if (!(factor >= 1.01 && factor <= 11)) {
|
|
110
|
+
throw new RangeError(`priceFactor: ${percent} % is outside 1.01..11`);
|
|
111
|
+
}
|
|
112
|
+
return factor;
|
|
113
|
+
}
|
|
114
|
+
export function addressAt(batch, index) {
|
|
115
|
+
return index < 0 ? null : (batch.addresses[index] ?? null);
|
|
116
|
+
}
|
|
117
|
+
export function addressIndex(batch, address) {
|
|
118
|
+
return batch.addresses.indexOf(address);
|
|
119
|
+
}
|
|
120
|
+
export function mintOf(batch, i) {
|
|
121
|
+
return addressAt(batch, batch.mint[i] ?? NO_ADDRESS) ?? "";
|
|
122
|
+
}
|
|
123
|
+
export function traderOf(batch, i) {
|
|
124
|
+
return addressAt(batch, batch.trader[i] ?? NO_ADDRESS);
|
|
125
|
+
}
|
|
126
|
+
export function poolOf(batch, i) {
|
|
127
|
+
return addressAt(batch, batch.pool[i] ?? NO_ADDRESS);
|
|
128
|
+
}
|
|
129
|
+
export function quoteMintOf(batch, i) {
|
|
130
|
+
return addressAt(batch, batch.quoteMint[i] ?? NO_ADDRESS) ?? "";
|
|
131
|
+
}
|
|
132
|
+
export function sideOf(batch, i) {
|
|
133
|
+
if (batch.side[i] === Side.event)
|
|
134
|
+
throw new RangeError(`Row ${i} is a pool event (withEvents), not a trade`);
|
|
135
|
+
return batch.side[i] === Side.sell ? "sell" : "buy";
|
|
136
|
+
}
|
|
137
|
+
export function protocolOf(batch, i) {
|
|
138
|
+
return batch.protocol[i] === Protocol.pumpswap ? "pumpswap" : "pumpfun";
|
|
139
|
+
}
|
|
140
|
+
export function compareChainOrder(a, i, b, j) {
|
|
141
|
+
const x = a.order, y = b.order;
|
|
142
|
+
if (!x || !y)
|
|
143
|
+
throw new RangeError("compareChainOrder needs batches read withOrder: true");
|
|
144
|
+
return ((a.slot[i] ?? 0) - (b.slot[j] ?? 0) ||
|
|
145
|
+
(x.txIndex[i] ?? NO_TX_INDEX) - (y.txIndex[j] ?? NO_TX_INDEX) ||
|
|
146
|
+
(x.eventIndex[i] ?? NO_EVENT_INDEX) - (y.eventIndex[j] ?? NO_EVENT_INDEX) ||
|
|
147
|
+
a.hour - b.hour ||
|
|
148
|
+
(a.line[i] ?? 0) - (b.line[j] ?? 0) ||
|
|
149
|
+
(a.op[i] ?? 0) - (b.op[j] ?? 0));
|
|
150
|
+
}
|
|
151
|
+
export function hourStartMs(hour) {
|
|
152
|
+
return hour * HOUR_MS;
|
|
153
|
+
}
|
|
154
|
+
const orNull = (value) => value === undefined || Number.isNaN(value) ? null : value;
|
|
155
|
+
export function tradeAt(batch, i) {
|
|
156
|
+
if (!Number.isInteger(i) || i < 0 || i >= batch.length)
|
|
157
|
+
throw new RangeError(`Row ${i} out of range (length ${batch.length})`);
|
|
158
|
+
return {
|
|
159
|
+
id: tradeId(batch, i),
|
|
160
|
+
hour: batch.hour,
|
|
161
|
+
line: batch.line[i],
|
|
162
|
+
op: batch.op[i],
|
|
163
|
+
slot: batch.slot[i],
|
|
164
|
+
timeMs: batch.timeMs[i],
|
|
165
|
+
receivedMs: orNull(batch.receivedMs[i]),
|
|
166
|
+
side: sideOf(batch, i),
|
|
167
|
+
protocol: protocolOf(batch, i),
|
|
168
|
+
mint: mintOf(batch, i),
|
|
169
|
+
pool: poolOf(batch, i),
|
|
170
|
+
trader: traderOf(batch, i),
|
|
171
|
+
quoteMint: quoteMintOf(batch, i),
|
|
172
|
+
tokenRaw: batch.tokenRaw[i],
|
|
173
|
+
tokenDecimals: batch.tokenDecimals[i],
|
|
174
|
+
quoteRaw: batch.quoteRaw[i],
|
|
175
|
+
quoteDecimals: batch.quoteDecimals[i],
|
|
176
|
+
tokenAmount: batch.tokenAmount[i],
|
|
177
|
+
quoteAmount: batch.quoteAmount[i],
|
|
178
|
+
price: orNull(batch.price[i]),
|
|
179
|
+
priceSol: orNull(batch.priceSol[i]),
|
|
180
|
+
priceUsd: orNull(batch.priceUsd[i]),
|
|
181
|
+
quotePriceUsd: orNull(batch.quotePriceUsd[i]),
|
|
182
|
+
usdValue: orNull(batch.usdValue[i]),
|
|
183
|
+
signature: batch.signatures?.[i] ?? null,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
export const STORAGE_STATUS_BY_CODE = {
|
|
187
|
+
blocked_type: 415,
|
|
188
|
+
forbidden: 403,
|
|
189
|
+
invalid_request: 400,
|
|
190
|
+
not_found: 404,
|
|
191
|
+
quota_exceeded: 413,
|
|
192
|
+
too_large: 413,
|
|
193
|
+
unavailable: 503,
|
|
194
|
+
already_exists: 409,
|
|
195
|
+
value_changed: 409,
|
|
196
|
+
io_limit: 429,
|
|
197
|
+
upload_limit: 429,
|
|
198
|
+
precondition_failed: 412,
|
|
199
|
+
};
|
|
200
|
+
export function isSdkError(error, code) {
|
|
201
|
+
if (!(error instanceof Error))
|
|
202
|
+
return false;
|
|
203
|
+
const actual = error.code;
|
|
204
|
+
return typeof actual === "string" && (code === undefined || actual === code);
|
|
205
|
+
}
|
|
206
|
+
function isRecord(value) {
|
|
207
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
208
|
+
}
|
package/dist/stream.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Stream } from "effect";
|
|
2
|
+
import { ExecutionEvent, Gap } from "./contracts.js";
|
|
3
|
+
import { DecodeError, NetworkError } from "./errors.js";
|
|
4
|
+
export { ExecutionEvent, Gap } from "./contracts.js";
|
|
5
|
+
export type FunctionsMessage = {
|
|
6
|
+
readonly _tag: "Execution";
|
|
7
|
+
readonly event: ExecutionEvent;
|
|
8
|
+
} | {
|
|
9
|
+
readonly _tag: "Gap";
|
|
10
|
+
readonly gap: Gap;
|
|
11
|
+
};
|
|
12
|
+
export type StreamError = DecodeError | NetworkError;
|
|
13
|
+
export interface StreamOptions {
|
|
14
|
+
readonly url: string;
|
|
15
|
+
readonly apiKey: string;
|
|
16
|
+
/** Resume after this sequence number. */
|
|
17
|
+
readonly afterSeq?: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Live execution events of the account. Gaps are emitted as values; the
|
|
21
|
+
* stream fails on undecodable payloads or when the server refuses the join.
|
|
22
|
+
* The socket is closed when the stream is interrupted or ends.
|
|
23
|
+
*/
|
|
24
|
+
export declare const executionEvents: (options: StreamOptions) => Stream.Stream<FunctionsMessage, StreamError>;
|
|
25
|
+
/** Callback API kept for existing users; errors are tagged `StreamError`s or raw socket errors. */
|
|
26
|
+
export declare function subscribeFunctions(options: {
|
|
27
|
+
url: string;
|
|
28
|
+
apiKey: string;
|
|
29
|
+
afterSeq?: number;
|
|
30
|
+
onEvent: (event: ExecutionEvent) => void;
|
|
31
|
+
onGap?: (gap: Gap) => void;
|
|
32
|
+
onError?: (error: unknown) => void;
|
|
33
|
+
}): {
|
|
34
|
+
close(): void;
|
|
35
|
+
readonly cursor: number | undefined;
|
|
36
|
+
};
|
package/dist/stream.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Socket } from "phoenix";
|
|
2
|
+
import { Cause, Effect, Queue, Result, Stream } from "effect";
|
|
3
|
+
import { ExecutionEvent, Gap, decodeWith, } from "./contracts.js";
|
|
4
|
+
import { DecodeError, NetworkError } from "./errors.js";
|
|
5
|
+
export { ExecutionEvent, Gap } from "./contracts.js";
|
|
6
|
+
const decoder = (schema, what) => decodeWith(schema, undefined, (message) => new DecodeError({ what, message }));
|
|
7
|
+
const decodeEvent = decoder(ExecutionEvent, "execution event");
|
|
8
|
+
const decodeGap = decoder(Gap, "stream gap");
|
|
9
|
+
function connect(options, handlers) {
|
|
10
|
+
let cursor = options.afterSeq;
|
|
11
|
+
const endpoint = new URL("/v1/stream", options.url);
|
|
12
|
+
endpoint.protocol =
|
|
13
|
+
endpoint.protocol === "https:" || endpoint.protocol === "wss:"
|
|
14
|
+
? "wss:"
|
|
15
|
+
: "ws:";
|
|
16
|
+
const socket = new Socket(endpoint.toString(), {
|
|
17
|
+
params: { api_key: options.apiKey },
|
|
18
|
+
transport: WebSocket,
|
|
19
|
+
});
|
|
20
|
+
const channel = socket.channel("functions", () => cursor === undefined ? {} : { after_seq: cursor });
|
|
21
|
+
channel.on("execution", (raw) => {
|
|
22
|
+
const result = decodeEvent(raw);
|
|
23
|
+
if (Result.isFailure(result))
|
|
24
|
+
return handlers.fail(result.failure);
|
|
25
|
+
const event = result.success;
|
|
26
|
+
if (cursor !== undefined && event.seq <= cursor)
|
|
27
|
+
return;
|
|
28
|
+
cursor = event.seq;
|
|
29
|
+
handlers.message({ _tag: "Execution", event });
|
|
30
|
+
});
|
|
31
|
+
channel.on("gap", (raw) => {
|
|
32
|
+
const result = decodeGap(raw);
|
|
33
|
+
if (Result.isFailure(result))
|
|
34
|
+
return handlers.fail(result.failure);
|
|
35
|
+
cursor = result.success.resumeAfter;
|
|
36
|
+
handlers.message({ _tag: "Gap", gap: result.success });
|
|
37
|
+
});
|
|
38
|
+
socket.onError((error) => handlers.transient(error));
|
|
39
|
+
channel.onError((error) => handlers.transient(error));
|
|
40
|
+
socket.connect();
|
|
41
|
+
channel.join().receive("error", (reason) => handlers.fail(new NetworkError({
|
|
42
|
+
url: endpoint.toString(),
|
|
43
|
+
message: `Stream join refused: ${JSON.stringify(reason)}`,
|
|
44
|
+
cause: reason,
|
|
45
|
+
})));
|
|
46
|
+
return {
|
|
47
|
+
close() {
|
|
48
|
+
channel.leave();
|
|
49
|
+
socket.disconnect();
|
|
50
|
+
},
|
|
51
|
+
get cursor() {
|
|
52
|
+
return cursor;
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export const executionEvents = (options) => Stream.callback((queue) => Effect.acquireRelease(Effect.sync(() => connect(options, {
|
|
57
|
+
message: (message) => void Queue.offerUnsafe(queue, message),
|
|
58
|
+
fail: (error) => void Queue.failCauseUnsafe(queue, Cause.fail(error)),
|
|
59
|
+
transient: () => { },
|
|
60
|
+
})), (connection) => Effect.sync(() => connection.close())));
|
|
61
|
+
export function subscribeFunctions(options) {
|
|
62
|
+
return connect(options, {
|
|
63
|
+
message: (message) => message._tag === "Execution"
|
|
64
|
+
? options.onEvent(message.event)
|
|
65
|
+
: options.onGap?.(message.gap),
|
|
66
|
+
fail: (error) => options.onError?.(error),
|
|
67
|
+
transient: (error) => options.onError?.(error),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** What a webhook delivers (the JSON body). */
|
|
2
|
+
export interface WebhookMessage {
|
|
3
|
+
/** Unique per message, the same on every delivery attempt: dedup on it. */
|
|
4
|
+
id: string;
|
|
5
|
+
/** Per account, 1, 2, 3… without holes; a hole is a message that failed for good. */
|
|
6
|
+
seq: number;
|
|
7
|
+
/** `send` (`ctx.webhook.send`), `execution.failed`, `execution.limit` or `test`. */
|
|
8
|
+
type: "send" | "execution.failed" | "execution.limit" | "test";
|
|
9
|
+
createdAt: number;
|
|
10
|
+
/** The payload of `ctx.webhook.send`, or `{ error }` for a failure. */
|
|
11
|
+
data: unknown;
|
|
12
|
+
/** Execution that produced it (absent for `test`). */
|
|
13
|
+
executionId?: string;
|
|
14
|
+
/** Attempt of that execution: a retried execution sends again with a higher one. */
|
|
15
|
+
attempt?: number;
|
|
16
|
+
/** Rank of the `send` call in that attempt (0, 1, …). */
|
|
17
|
+
index?: number;
|
|
18
|
+
program?: string;
|
|
19
|
+
function?: string;
|
|
20
|
+
workflow?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface VerifyOptions {
|
|
23
|
+
/** Maximum age (and clock skew) of the signature, in seconds (default 300). */
|
|
24
|
+
toleranceSec?: number;
|
|
25
|
+
/** Current time in ms (tests). */
|
|
26
|
+
now?: number;
|
|
27
|
+
}
|
|
28
|
+
export declare class WebhookVerificationError extends Error {
|
|
29
|
+
readonly code: "missing_signature" | "malformed_signature" | "timestamp_out_of_tolerance" | "invalid_signature" | "invalid_body";
|
|
30
|
+
constructor(code: WebhookVerificationError["code"], message: string);
|
|
31
|
+
}
|
|
32
|
+
type HeaderBag = Headers | Record<string, string | string[] | undefined> | Map<string, string>;
|
|
33
|
+
/**
|
|
34
|
+
* Checks `x-zyrk-signature` (`t=<unix>,v1=<hex>[,v1=<hex>]`, HMAC-SHA256 of
|
|
35
|
+
* `t + "." + body` with your secret) and returns the parsed message. Pass
|
|
36
|
+
* the **raw** body, exactly as received. During a secret rotation the
|
|
37
|
+
* header carries one `v1` per valid secret: either of your secrets passes.
|
|
38
|
+
* Pass an array to accept several secrets on your side too.
|
|
39
|
+
*
|
|
40
|
+
* Throws {@link WebhookVerificationError}.
|
|
41
|
+
*/
|
|
42
|
+
export declare const verifyWebhook: (secret: string | readonly string[], headers: HeaderBag, body: string | Uint8Array, options?: VerifyOptions) => WebhookMessage;
|
|
43
|
+
export {};
|
package/dist/webhook.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
export class WebhookVerificationError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
constructor(code, message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "WebhookVerificationError";
|
|
7
|
+
this.code = code;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
const header = (headers, name) => {
|
|
11
|
+
if (typeof headers.get === "function") {
|
|
12
|
+
const v = headers.get(name);
|
|
13
|
+
return v ?? undefined;
|
|
14
|
+
}
|
|
15
|
+
const record = headers;
|
|
16
|
+
const key = Object.keys(record).find((k) => k.toLowerCase() === name);
|
|
17
|
+
const v = key === undefined ? undefined : record[key];
|
|
18
|
+
return Array.isArray(v) ? v[0] : v;
|
|
19
|
+
};
|
|
20
|
+
export const verifyWebhook = (secret, headers, body, options = {}) => {
|
|
21
|
+
const text = typeof body === "string" ? body : new TextDecoder().decode(body);
|
|
22
|
+
const raw = header(headers, "x-zyrk-signature");
|
|
23
|
+
if (!raw)
|
|
24
|
+
throw new WebhookVerificationError("missing_signature", "No x-zyrk-signature header");
|
|
25
|
+
let timestamp;
|
|
26
|
+
const signatures = [];
|
|
27
|
+
for (const part of raw.split(",")) {
|
|
28
|
+
const [k, v] = part.trim().split("=", 2);
|
|
29
|
+
if (k === "t" && v !== undefined && /^\d+$/.test(v))
|
|
30
|
+
timestamp = Number(v);
|
|
31
|
+
else if (k === "v1" && v !== undefined && /^[0-9a-f]{64}$/.test(v))
|
|
32
|
+
signatures.push(v);
|
|
33
|
+
}
|
|
34
|
+
if (timestamp === undefined || signatures.length === 0)
|
|
35
|
+
throw new WebhookVerificationError("malformed_signature", "Expected t=<unix>,v1=<hex>");
|
|
36
|
+
const tolerance = options.toleranceSec ?? 300;
|
|
37
|
+
const now = Math.floor((options.now ?? Date.now()) / 1000);
|
|
38
|
+
if (Math.abs(now - timestamp) > tolerance)
|
|
39
|
+
throw new WebhookVerificationError("timestamp_out_of_tolerance", `Signature timestamp is more than ${tolerance} s away from now`);
|
|
40
|
+
const secrets = typeof secret === "string" ? [secret] : secret;
|
|
41
|
+
const signed = `${timestamp}.${text}`;
|
|
42
|
+
const ok = secrets.some((s) => {
|
|
43
|
+
const expected = createHmac("sha256", s).update(signed).digest();
|
|
44
|
+
return signatures.some((candidate) => timingSafeEqual(expected, Buffer.from(candidate, "hex")));
|
|
45
|
+
});
|
|
46
|
+
if (!ok)
|
|
47
|
+
throw new WebhookVerificationError("invalid_signature", "No signature matches the secret");
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(text);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
throw new WebhookVerificationError("invalid_body", "Body is not JSON");
|
|
53
|
+
}
|
|
54
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,58 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johannc-dev/zyrk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "SDK and CLI for Zyrk hosted functions (funrun v2)",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public",
|
|
8
|
+
"provenance": false
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"types": "./dist/sdk.d.ts",
|
|
12
|
+
"files": [
|
|
13
|
+
"dist/*.js",
|
|
14
|
+
"dist/*.d.ts",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"exports": {
|
|
18
|
+
".": "./dist/sdk.js",
|
|
19
|
+
"./client": "./dist/client.js",
|
|
20
|
+
"./stream": "./dist/stream.js",
|
|
21
|
+
"./contracts": "./dist/contracts.js",
|
|
22
|
+
"./errors": "./dist/errors.js",
|
|
23
|
+
"./build": "./dist/build.js",
|
|
24
|
+
"./limits": "./dist/limits.js",
|
|
25
|
+
"./local": "./dist/local.js",
|
|
26
|
+
"./export": "./dist/export.js",
|
|
27
|
+
"./hourfile": "./dist/hourfile.js",
|
|
28
|
+
"./webhook": "./dist/webhook.js",
|
|
29
|
+
"./globals": {
|
|
30
|
+
"types": "./dist/globals.d.ts"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"bin": {
|
|
34
|
+
"zyrk": "./dist/cli.js"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=24"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "node scripts/build.mjs",
|
|
41
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
|
|
42
|
+
"test": "npm run build && npm run typecheck && node --test test/*.test.ts",
|
|
43
|
+
"format": "prettier --write src types scripts test examples package.json tsconfig.json tsconfig.test.json README.md",
|
|
44
|
+
"format:check": "prettier --check src types scripts test examples package.json tsconfig.json tsconfig.test.json README.md",
|
|
45
|
+
"prepack": "npm run build"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"effect": "4.1.2",
|
|
49
|
+
"esbuild": "0.28.2",
|
|
50
|
+
"phoenix": "1.7.24",
|
|
51
|
+
"typescript": "5.9.2"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/node": "24.10.0",
|
|
55
|
+
"@types/phoenix": "1.6.8",
|
|
56
|
+
"prettier": "3.6.2"
|
|
57
|
+
}
|
|
58
|
+
}
|