rosefc 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.
@@ -0,0 +1,149 @@
1
+ // LoRaWAN PHYPayload parser (v1.0.x / v1.1 on the wire, structural).
2
+ //
3
+ // Zero-dependency, Buffer-free (Uint8Array + DataView only) so the same module
4
+ // is safe in the browser bundle and in a prerender - it reads bytes, never
5
+ // touches the network or a key. The FRMPayload of a data frame is
6
+ // application-encrypted (AES-128 with AppSKey); this module returns it raw -
7
+ // decrypting is the app's business, because the key is the app's secret.
8
+ //
9
+ // PHYPayload = MHDR(1) | MACPayload | MIC(4)
10
+ // data frame MACPayload = DevAddr(4,LE) FCtrl(1) FCnt(2,LE) FOpts(0-15) [FPort(1)] FRMPayload
11
+
12
+ export type LoRaWANMessageType =
13
+ | 'join-request'
14
+ | 'join-accept'
15
+ | 'unconfirmed-data-up'
16
+ | 'unconfirmed-data-down'
17
+ | 'confirmed-data-up'
18
+ | 'confirmed-data-down'
19
+ | 'proprietary';
20
+
21
+ export interface LoRaWANDataFrame {
22
+ kind: 'data';
23
+ mtype: LoRaWANMessageType;
24
+ major: 0 | 1;
25
+ devAddr: string; // 8 hex chars, the little-endian word as LoRaWAN prints it
26
+ fctrl: { adr: boolean; adrAckReq: boolean; ack: boolean; classB: boolean; fOptsLen: number };
27
+ fCnt: number;
28
+ fOpts: Uint8Array | null;
29
+ fPort: number | null; // null when the frame carries no port byte
30
+ frmPayload: Uint8Array | null; // still encrypted; null when absent
31
+ mic: Uint8Array;
32
+ }
33
+
34
+ export interface LoRaWANJoinRequest {
35
+ kind: 'join-request';
36
+ major: 0 | 1;
37
+ appEUI: string; // 16 hex chars
38
+ devEUI: string; // 16 hex chars
39
+ devNonce: number;
40
+ mic: Uint8Array;
41
+ }
42
+
43
+ export interface LoRaWANJoinAccept {
44
+ kind: 'join-accept';
45
+ major: 0 | 1;
46
+ appNonce: string; // 6 hex chars
47
+ netID: string; // 6 hex chars
48
+ devAddr: string; // 8 hex chars
49
+ mic: Uint8Array;
50
+ }
51
+
52
+ export type LoRaWANFrame = LoRaWANDataFrame | LoRaWANJoinRequest | LoRaWANJoinAccept;
53
+
54
+ const MTYPES: Record<number, LoRaWANMessageType> = {
55
+ 0: 'join-request',
56
+ 1: 'join-accept',
57
+ 2: 'unconfirmed-data-up',
58
+ 3: 'unconfirmed-data-down',
59
+ 4: 'confirmed-data-up',
60
+ 5: 'confirmed-data-down',
61
+ 6: 'proprietary',
62
+ 7: 'proprietary',
63
+ };
64
+
65
+ const HEX = '0123456789ABCDEF';
66
+ function hex(bytes: Uint8Array): string {
67
+ let out = '';
68
+ for (let i = 0; i < bytes.length; i++) {
69
+ out += HEX[bytes[i] >> 4] + HEX[bytes[i] & 0x0f];
70
+ }
71
+ return out;
72
+ }
73
+
74
+ /**
75
+ * Parse a LoRaWAN PHYPayload. Returns null for anything that is not a
76
+ * well-formed frame (too short for its own headers, a truncated MIC).
77
+ * Throws nothing - a gateway loop must never die on one bad datagram.
78
+ */
79
+ export function parseLoRaWAN(frame: Uint8Array): LoRaWANFrame | null {
80
+ if (!frame || frame.length < 5) return null;
81
+ const mhdr = frame[0];
82
+ const mtype = MTYPES[(mhdr >> 5) & 0x07];
83
+ const major = (mhdr & 0x03) as 0 | 1;
84
+ if (major > 1) return null;
85
+ const body = frame.subarray(1, frame.length - 4);
86
+ const mic = frame.subarray(frame.length - 4);
87
+ if (body.length < 1) return null;
88
+ const dv = new DataView(frame.buffer, frame.byteOffset, frame.byteLength);
89
+
90
+ if (mtype === 'join-request') {
91
+ if (body.length !== 18) return null;
92
+ return {
93
+ kind: 'join-request',
94
+ major,
95
+ appEUI: hex(body.subarray(0, 8)),
96
+ devEUI: hex(body.subarray(8, 16)),
97
+ devNonce: dv.getUint16(17, true),
98
+ mic,
99
+ };
100
+ }
101
+
102
+ if (mtype === 'join-accept') {
103
+ if (body.length < 12) return null;
104
+ return {
105
+ kind: 'join-accept',
106
+ major,
107
+ appNonce: hex(body.subarray(0, 3)),
108
+ netID: hex(body.subarray(3, 6)),
109
+ devAddr: hex(body.subarray(6, 10)),
110
+ mic,
111
+ };
112
+ }
113
+
114
+ // data frames (up/down, confirmed or not)
115
+ if (body.length < 7) return null;
116
+ const devAddr = hex(body.subarray(0, 4));
117
+ const fctrlB = body[4];
118
+ const fOptsLen = fctrlB & 0x0f;
119
+ const fCnt = dv.getUint16(6, true);
120
+ let p = 7;
121
+ if (p + fOptsLen > body.length) return null;
122
+ const fOpts = fOptsLen > 0 ? body.subarray(p, p + fOptsLen) : null;
123
+ p += fOptsLen;
124
+ let fPort: number | null = null;
125
+ let frmPayload: Uint8Array | null = null;
126
+ if (p < body.length) {
127
+ fPort = body[p];
128
+ p += 1;
129
+ if (p < body.length) frmPayload = body.subarray(p);
130
+ }
131
+ return {
132
+ kind: 'data',
133
+ mtype,
134
+ major,
135
+ devAddr,
136
+ fctrl: {
137
+ adr: (fctrlB & 0x80) !== 0,
138
+ adrAckReq: (fctrlB & 0x40) !== 0,
139
+ ack: (fctrlB & 0x20) !== 0,
140
+ classB: (fctrlB & 0x10) !== 0,
141
+ fOptsLen,
142
+ },
143
+ fCnt,
144
+ fOpts,
145
+ fPort,
146
+ frmPayload,
147
+ mic,
148
+ };
149
+ }
@@ -0,0 +1,327 @@
1
+
2
+ /// <reference lib="dom" />
3
+ /// <reference types="node" />
4
+ type Getter<T> = () => T;
5
+
6
+ /** A store shared by every request in this process (and, with a transport, every worker). */
7
+ interface Store<T> {
8
+ get(): T;
9
+ set(v: T): void;
10
+ update(fn: (v: T) => T): void;
11
+ }
12
+
13
+ /** Declare reactive state. Server-rendered into the document, resumed by the client with zero hydration. */
14
+ declare function $state<T>(initial: T): Getter<T>;
15
+
16
+ /** Write reactive state by key - the same write the generated setter performs. */
17
+ declare function $setState<T>(key: string, value: T): void;
18
+
19
+ /**
20
+ * Computed state: compiles to a plain getter over the signals the expression
21
+ * reads, so every read recomputes - no subscription machinery, never
22
+ * serialized. Top-level declarations only. The expression is Rosefd syntax
23
+ * (bare state reads are callified at compile time), so it checks as `any`.
24
+ */
25
+ declare function $derived(expr: any): Getter<any>;
26
+
27
+ /** Like $state, but every write mirrors to localStorage (JSON, `rosefd:` prefix) and boot re-seeds from it. */
28
+ declare function $persist<T>(initial: T): Getter<T>;
29
+
30
+ /**
31
+ * Declaration form: `let x = $kv(() => expensive(), 60)`. The compiler turns
32
+ * it into seeded $state backed by the server-side memo - the value arrives
33
+ * with the first render and every request inside the TTL shares one compute.
34
+ */
35
+ declare function $kv<T>(compute: () => T | Promise<T>, ttlSeconds?: number): Getter<T>;
36
+
37
+
38
+ declare function $data<T>(fn: () => T | Promise<T>): Getter<T>;
39
+
40
+ /** The per-request bag filled by `pages/_middleware.rose` (auth, db, cache). */
41
+ declare function getContext<T = Record<string, unknown>>(): T;
42
+
43
+ // The parsed query string of the request being rendered (`?page=2&q=x` -> `{ page: '2', q: 'x' }`).
44
+ declare function $query(): Record<string, string>;
45
+
46
+
47
+ declare function $t(key: string, vars?: Record<string, string | number>): string;
48
+
49
+ /** 'rtl' for a right-to-left locale (ar, he, fa, ur, ...), 'ltr' otherwise. */
50
+ declare function localeDir(lang: string): string;
51
+
52
+
53
+ declare function ensureLocale(lang: string): Promise<void>;
54
+
55
+ /** Merge a dictionary that arrived at runtime (a fetched pack, or your own import()). */
56
+ declare function loadLocale(lang: string, dict: Record<string, string>): void;
57
+
58
+ /** A named, process-wide store. Values must be JSON-serializable. */
59
+ declare function $store<T>(name: string, initial: T): Store<T>;
60
+
61
+ /** Parse the request's cookies (server) or this document's (client). */
62
+ declare function $cookies(request?: Request): Record<string, string>;
63
+
64
+ /** Build a Set-Cookie value with the safe defaults: HttpOnly, SameSite=Lax, Path=/, 7 days. */
65
+ declare function $sessionCookie(
66
+ name: string,
67
+ value: string,
68
+ opts?: { path?: string; maxAge?: number; sameSite?: 'Strict' | 'Lax' | 'None'; secure?: boolean; httpOnly?: boolean }
69
+ ): string;
70
+
71
+ /** Run once after the client has adopted the server's DOM. */
72
+ declare function onMount(fn: () => void): void;
73
+
74
+ /** Register a teardown for the next navigation. */
75
+ declare function onCleanup(fn: () => void): void;
76
+
77
+ declare function $effect(fn: () => void | (() => void)): () => void;
78
+
79
+ /** Re-render the current route in place - `$data` re-runs, zero requests. */
80
+ declare function refresh(): Promise<void>;
81
+
82
+ /** HTML-escape a value into a string (the same escape the template markers use). */
83
+ declare function esc(v: unknown): string;
84
+
85
+ /** Call a server action by name; the response is adopted in place. */
86
+ declare function $action(name: string, e?: Event): Promise<void>;
87
+
88
+
89
+ declare class ActionError extends Error {
90
+ code: number;
91
+ field?: string;
92
+ constructor(message: string, code?: number, field?: string);
93
+ }
94
+
95
+ /** What the dispatch seeds into state when an action fails (what `$actionError()` returns). */
96
+ interface ActionErrorInfo {
97
+ action: string;
98
+ message: string;
99
+ code: number;
100
+ field?: string;
101
+ }
102
+
103
+ // The failure of the action that produced the response being rendered, or null.
104
+ declare function $actionError(): ActionErrorInfo | null;
105
+
106
+ // An incremental patch: an action's return value may map a state key to an OPERATION instead of a whole replacement.
107
+ declare function $append(value: unknown): unknown;
108
+ declare function $prepend(value: unknown): unknown;
109
+ declare function $merge(value: Record<string, unknown>): unknown;
110
+
111
+ /** Whether a state key already holds a value (what `$data` checks before fetching). */
112
+ declare function hasState(key: string): boolean;
113
+
114
+ /** The best dictionary for an Accept-Language header (null when none matches). */
115
+ declare function bestLocale(header: string | null | undefined): string | null;
116
+
117
+ // === Globals the compiler injects when a route uses them ===
118
+ // These live on globalThis in the built bundle; declaring them here keeps
119
+ // `rosefd check` honest about code that reaches for them directly.
120
+
121
+ declare var __rosefdAuth: {
122
+ hashPassword(pw: string): string;
123
+ verifyPassword(pw: string, hash: string): boolean;
124
+ signSession(data: Record<string, unknown>, secret: string, ttlSeconds?: number): string;
125
+ verifySession<T = Record<string, unknown>>(token: string, secret: string): T | null;
126
+ csrfToken(secret: string, sessionId: string): string;
127
+ verifyCsrf(token: string, secret: string, sessionId: string): boolean;
128
+ };
129
+
130
+ declare var __rosefdKvApi: {
131
+ kv: {
132
+ get(key: string): unknown;
133
+ set(key: string, value: unknown, ttlSeconds?: number): void;
134
+ del(key: string): void;
135
+ has(key: string): boolean;
136
+ };
137
+ kvMemo: typeof $kv;
138
+ };
139
+
140
+ interface RosefdContentEntry {
141
+ slug: string;
142
+ title?: string;
143
+ date?: string;
144
+ tags?: string[];
145
+ excerpt?: string;
146
+ body: string;
147
+ html?: string;
148
+ toc?: unknown[];
149
+ readingTime?: number;
150
+ wordCount?: number;
151
+ [key: string]: unknown;
152
+ }
153
+
154
+ declare var __rosefdContent: {
155
+ list(coll: string, filter?: { tag: string }): Array<Record<string, unknown>>;
156
+ rss(coll: string, opts: { title: string; origin: string; description: string }): string;
157
+ get(coll: string, slug: string, vars?: Record<string, string>): RosefdContentEntry | null;
158
+ };
159
+
160
+ declare var __rosefdExtras: {
161
+ v: {
162
+ validate(body: Record<string, unknown>, rules: Record<string, string[]>): {
163
+ ok: boolean;
164
+ errors: Record<string, string>;
165
+ values: Record<string, unknown>;
166
+ };
167
+ };
168
+ };
169
+
170
+ /** Named view transitions: `globalThis.__rosefdTransitions.pulse = (el, ms) => () => {}` - used by `transition:pulse` in templates. */
171
+ declare var __rosefdTransitions: Record<string, (el: HTMLElement, duration: number) => () => void>;
172
+
173
+
174
+ declare function redirect(path: string, status?: 301 | 302 | 303 | 307 | 308): never;
175
+
176
+
177
+ declare function notFound(): never;
178
+
179
+ // === Network listeners (isomorphic) ===
180
+ // On the server these open real sockets when the serve worker boots
181
+ // (`__startNetwork`); on the client every handle is an inert no-op, so a page
182
+ // that opens a listener hydrates without a ReferenceError.
183
+
184
+ /** The sender of a UDP datagram (what `rinfo` carries in `onMessage`). */
185
+ interface RemoteInfo {
186
+ address: string;
187
+ family: string;
188
+ port: number;
189
+ size: number;
190
+ }
191
+
192
+ interface UDPSocket {
193
+ send(msg: Buffer, port: number, host?: string): void;
194
+ close(): void;
195
+ address(): { port: number; host: string; family: string };
196
+ joinMulticast(multicastAddress: string): void;
197
+ leaveMulticast(multicastAddress: string): void;
198
+ }
199
+
200
+ /**
201
+ * Listen for UDP datagrams (LoRa gateways, sensor feeds) from inside a Rosefd
202
+ * app. Only registers the spec; the framework binds it at serve-worker boot, so
203
+ * a build/prerender never opens a socket.
204
+ */
205
+ declare function $udp(
206
+ port: number,
207
+ opts?: {
208
+ host?: string;
209
+ onMessage?: (msg: Buffer, rinfo: RemoteInfo) => void;
210
+ onError?: (err: Error) => void;
211
+ }
212
+ ): UDPSocket;
213
+
214
+ interface TCPsocket {
215
+ write(data: string | Buffer): void;
216
+ end(data?: string | Buffer): void;
217
+ destroy(): void;
218
+ on(event: 'data', fn: (data: Buffer) => void): void;
219
+ on(event: 'close', fn: () => void): void;
220
+ on(event: 'error', fn: (err: Error) => void): void;
221
+ readonly remoteAddress?: string;
222
+ }
223
+
224
+ interface TCPServer {
225
+ close(): void;
226
+ address(): { port: number; host: string };
227
+ }
228
+
229
+ /**
230
+ * A TCP server (a number, or opts with onConnection) or a TCP client (a host
231
+ * string, or opts with onConnect). Same register-at-boot discipline as $udp.
232
+ */
233
+ declare function $tcp(
234
+ portOrHost: number | string,
235
+ opts?: {
236
+ port?: number;
237
+ host?: string;
238
+ idleTimeout?: number;
239
+ onConnection?: (socket: TCPsocket) => void;
240
+ onData?: (socket: TCPsocket, data: Buffer) => void;
241
+ onIdle?: (socket: TCPsocket) => void;
242
+ onConnect?: () => void;
243
+ onClose?: () => void;
244
+ onError?: (err: Error) => void;
245
+ }
246
+ ): TCPServer | TCPsocket;
247
+
248
+ interface Listener {
249
+ close(): void;
250
+ address(): { port: number; host: string };
251
+ }
252
+
253
+ /** General-port listener: protocol 'http' | 'websocket' | 'udp' | 'tcp' (default 'http'). */
254
+ declare function $listen(
255
+ port: number,
256
+ opts?: {
257
+ protocol?: 'http' | 'websocket' | 'udp' | 'tcp';
258
+ host?: string;
259
+ onConnection?: (socket: unknown) => void;
260
+ onData?: (data: Buffer, remote: unknown) => void;
261
+ }
262
+ ): Listener;
263
+
264
+ /** One connection served by a `$websocket` server. */
265
+ interface WebSocket {
266
+ send(data: string | Buffer): void;
267
+ close(): void;
268
+ on(event: 'message', fn: (data: string | Buffer) => void): void;
269
+ on(event: 'close', fn: () => void): void;
270
+ on(event: 'error', fn: (err: Error) => void): void;
271
+ readonly remoteAddress?: string;
272
+ }
273
+
274
+ interface WebSocketServer {
275
+ close(): void;
276
+ clients(): WebSocket[];
277
+ broadcast(data: string | Buffer): void;
278
+ }
279
+
280
+ /**
281
+ * A zero-dependency RFC 6455 WebSocket server on its own port. The returned
282
+ * facade stays live: broadcast()/close() reach the bound server whenever the
283
+ * app calls them (on the client the facade is an inert no-op).
284
+ */
285
+ declare function $websocket(
286
+ port: number,
287
+ opts?: {
288
+ host?: string;
289
+ path?: string;
290
+ pingInterval?: number;
291
+ onConnection?: (ws: WebSocket) => void;
292
+ onMessage?: (ws: WebSocket, data: string | Buffer) => void;
293
+ onClose?: (ws: WebSocket) => void;
294
+ onError?: (err: Error) => void;
295
+ }
296
+ ): WebSocketServer;
297
+
298
+ interface MQTTClient {
299
+ connect(): Promise<void>;
300
+ disconnect(): void;
301
+ subscribe(topic: string, opts?: { qos?: 0 | 1 | 2 }): void;
302
+ unsubscribe(topic: string): void;
303
+ publish(topic: string, message: string | Buffer, opts?: { qos?: 0 | 1 | 2; retain?: boolean }): void;
304
+ readonly connected: boolean;
305
+ }
306
+
307
+ /**
308
+ * A zero-dependency MQTT 3.1.1 client (QoS 0/1). `broker` is a URL like
309
+ * `mqtt://localhost:1883`. Packets sent before CONNACK queue and flush on
310
+ * connect. On the client the facade is an inert no-op.
311
+ */
312
+ declare function $mqtt(
313
+ broker: string,
314
+ opts?: {
315
+ clientId?: string;
316
+ username?: string;
317
+ password?: string;
318
+ keepalive?: number;
319
+ clean?: boolean;
320
+ reconnect?: boolean;
321
+ reconnectDelay?: number;
322
+ reconnectMax?: number;
323
+ onConnect?: () => void;
324
+ onMessage?: (topic: string, message: Buffer) => void;
325
+ onError?: (err: Error) => void;
326
+ }
327
+ ): MQTTClient;
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.