pockethook-sdk 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 PocketHook
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,257 @@
1
+ # pockethook-sdk
2
+
3
+ SDK for building [PocketHook](https://pockethook.app)-compatible server responses. Parse incoming requests from the PocketHook iOS app and build properly formatted responses — including text messages, iOS Shortcut triggers, data payloads, and URLs.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ bun add pockethook-sdk
9
+ ```
10
+
11
+ ```bash
12
+ npm install pockethook-sdk
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ```ts
18
+ import { parseRequest, text, toResponse } from "pockethook-sdk";
19
+
20
+ Bun.serve({
21
+ port: 3000,
22
+ async fetch(req) {
23
+ const { sessionId, chatInput } = parseRequest(await req.json());
24
+ return toResponse(text(`Echo: ${chatInput}`));
25
+ },
26
+ });
27
+ ```
28
+
29
+ ## API
30
+
31
+ ### Response Builders
32
+
33
+ #### `text(msg)`
34
+
35
+ Build a simple text response.
36
+
37
+ ```ts
38
+ import { text } from "pockethook-sdk";
39
+
40
+ text("Hello from the server!");
41
+ // => [{ msg: "Hello from the server!" }]
42
+ ```
43
+
44
+ #### `shortcut(msg, name, data?)`
45
+
46
+ Build a response that triggers an iOS Shortcut.
47
+
48
+ ```ts
49
+ import { shortcut } from "pockethook-sdk";
50
+
51
+ shortcut("Starting backup...", "BackupPhotos", { destination: "icloud" });
52
+ // => [{ msg: "Starting backup...", shortcut: "BackupPhotos", data: { destination: "icloud" } }]
53
+ ```
54
+
55
+ #### `response(options)`
56
+
57
+ Build a single response with full control over all fields.
58
+
59
+ ```ts
60
+ import { response } from "pockethook-sdk";
61
+
62
+ response({
63
+ msg: "File ready",
64
+ shortcut: "OpenFile",
65
+ data: { path: "/documents/report.pdf" },
66
+ url: "https://example.com/report.pdf",
67
+ });
68
+ ```
69
+
70
+ #### `responses(items)`
71
+
72
+ Build multiple responses for sequential Shortcut execution. PocketHook runs them in order, passing results between Shortcuts.
73
+
74
+ ```ts
75
+ import { responses } from "pockethook-sdk";
76
+
77
+ responses([
78
+ { msg: "Downloading...", shortcut: "DownloadFile", data: { id: 42 } },
79
+ { msg: "Processing...", shortcut: "ConvertToPDF" },
80
+ { msg: "Done!", shortcut: "ShareFile" },
81
+ ]);
82
+ ```
83
+
84
+ ### Serialization
85
+
86
+ #### `toJSON(items)`
87
+
88
+ Serialize responses to a JSON string.
89
+
90
+ ```ts
91
+ import { response, toJSON } from "pockethook-sdk";
92
+
93
+ const json = toJSON([response({ msg: "Hello" })]);
94
+ // => '[{"msg":"Hello"}]'
95
+ ```
96
+
97
+ #### `toResponse(items, status?)`
98
+
99
+ Create a `Response` object with `Content-Type: application/json`. Ready to return from any server handler.
100
+
101
+ ```ts
102
+ import { text, toResponse } from "pockethook-sdk";
103
+
104
+ toResponse(text("Hello"));
105
+ // => Response { status: 200, headers: { "Content-Type": "application/json" } }
106
+
107
+ toResponse(text("Created"), 201);
108
+ // => Response { status: 201, ... }
109
+ ```
110
+
111
+ ### Request Parsing
112
+
113
+ #### `parseRequest(body)`
114
+
115
+ Parse and validate an incoming PocketHook request. Accepts a JSON string, `Uint8Array`, or already-parsed object.
116
+
117
+ ```ts
118
+ import { parseRequest } from "pockethook-sdk";
119
+
120
+ const { sessionId, chatInput } = parseRequest(await req.json());
121
+ ```
122
+
123
+ Validates:
124
+ - `sessionId` is a valid UUID
125
+ - `chatInput` is a non-empty string (max 10,000 characters)
126
+
127
+ #### `extractBearerToken(header)`
128
+
129
+ Extract the token from an `Authorization: Bearer <token>` header.
130
+
131
+ ```ts
132
+ import { extractBearerToken } from "pockethook-sdk";
133
+
134
+ const token = extractBearerToken(req.headers.get("Authorization"));
135
+ if (token !== process.env.AUTH_TOKEN) {
136
+ return new Response("Unauthorized", { status: 401 });
137
+ }
138
+ ```
139
+
140
+ #### `validateHttpsUrl(url)`
141
+
142
+ Validate that a URL uses HTTPS. PocketHook rejects non-HTTPS URLs.
143
+
144
+ ```ts
145
+ import { validateHttpsUrl } from "pockethook-sdk";
146
+
147
+ validateHttpsUrl("https://example.com"); // => "https://example.com"
148
+ validateHttpsUrl("http://example.com"); // throws Error
149
+ ```
150
+
151
+ ## Protocol Reference
152
+
153
+ ### Request Format
154
+
155
+ PocketHook sends a POST request with a JSON array:
156
+
157
+ ```json
158
+ [
159
+ {
160
+ "sessionId": "550e8400-e29b-41d4-a716-446655440000",
161
+ "action": "sendMessage",
162
+ "chatInput": "Hello server"
163
+ }
164
+ ]
165
+ ```
166
+
167
+ **Headers:**
168
+ - `Content-Type: application/json`
169
+ - `Authorization: Bearer <token>`
170
+
171
+ ### Response Format
172
+
173
+ The server must return a JSON array of response objects:
174
+
175
+ ```json
176
+ [
177
+ {
178
+ "msg": "Message to display in chat",
179
+ "shortcut": "ShortcutName",
180
+ "data": { "key": "value" },
181
+ "url": "https://example.com"
182
+ }
183
+ ]
184
+ ```
185
+
186
+ | Field | Type | Required | Description |
187
+ |-------|------|----------|-------------|
188
+ | `msg` | string | Yes | Message displayed in the PocketHook chat |
189
+ | `shortcut` | string | No | Name of the iOS Shortcut to trigger |
190
+ | `data` | object | No | Arbitrary data passed to the Shortcut |
191
+ | `url` | string | No | HTTPS URL associated with the response |
192
+
193
+ **Multiple actions:** Return multiple objects in the array. PocketHook executes shortcuts sequentially, in order.
194
+
195
+ ## Example: Full Server
196
+
197
+ ```ts
198
+ import { parseRequest, extractBearerToken, text, shortcut, responses, toResponse } from "pockethook-sdk";
199
+
200
+ const AUTH_TOKEN = process.env.AUTH_TOKEN!;
201
+
202
+ Bun.serve({
203
+ port: 3000,
204
+ async fetch(req) {
205
+ // Authenticate
206
+ const token = extractBearerToken(req.headers.get("Authorization"));
207
+ if (token !== AUTH_TOKEN) {
208
+ return new Response("Unauthorized", { status: 401 });
209
+ }
210
+
211
+ // Parse request
212
+ const { sessionId, chatInput } = parseRequest(await req.json());
213
+ const command = chatInput.toLowerCase().trim();
214
+
215
+ // Route commands
216
+ if (command === "backup") {
217
+ return toResponse(
218
+ shortcut("Starting backup...", "BackupPhotos", { date: new Date().toISOString() })
219
+ );
220
+ }
221
+
222
+ if (command === "deploy") {
223
+ return toResponse(
224
+ responses([
225
+ { msg: "Running tests...", shortcut: "RunTests" },
226
+ { msg: "Building...", shortcut: "BuildProject" },
227
+ { msg: "Deployed!", shortcut: "NotifyTeam", data: { channel: "general" } },
228
+ ])
229
+ );
230
+ }
231
+
232
+ return toResponse(text(`You said: ${chatInput}`));
233
+ },
234
+ });
235
+ ```
236
+
237
+ ## TypeScript Types
238
+
239
+ All types are exported for use in your own code:
240
+
241
+ ```ts
242
+ import type {
243
+ PocketHookRequest,
244
+ PocketHookResponse,
245
+ ResponseOptions,
246
+ ParsedRequest,
247
+ } from "pockethook-sdk";
248
+ ```
249
+
250
+ ## Requirements
251
+
252
+ - Bun >= 1.0 or Node.js >= 18
253
+ - TypeScript >= 5 (optional, for type checking)
254
+
255
+ ## License
256
+
257
+ MIT
@@ -0,0 +1,3 @@
1
+ export type { PocketHookRequest, PocketHookResponse, PocketHookWrappedResponse, ResponseOptions, ParsedRequest, } from "./types.js";
2
+ export { response, responses, text, shortcut, toJSON, toResponse, } from "./response.js";
3
+ export { parseRequest, validateHttpsUrl, extractBearerToken, } from "./validation.js";
package/dist/index.js ADDED
@@ -0,0 +1,110 @@
1
+ // src/validation.ts
2
+ var UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
3
+ function parseRequest(body) {
4
+ let parsed;
5
+ if (typeof body === "string") {
6
+ try {
7
+ parsed = JSON.parse(body);
8
+ } catch {
9
+ throw new Error("Invalid JSON in request body");
10
+ }
11
+ } else if (body instanceof ArrayBuffer || body instanceof Uint8Array) {
12
+ try {
13
+ const text = new TextDecoder().decode(body);
14
+ parsed = JSON.parse(text);
15
+ } catch {
16
+ throw new Error("Invalid JSON in request body");
17
+ }
18
+ } else {
19
+ parsed = body;
20
+ }
21
+ const arr = Array.isArray(parsed) ? parsed : [parsed];
22
+ const item = arr[0];
23
+ if (!item || typeof item !== "object") {
24
+ throw new Error("Request must contain at least one object");
25
+ }
26
+ const obj = item;
27
+ const sessionId = obj.sessionId;
28
+ if (typeof sessionId !== "string" || !UUID_REGEX.test(sessionId)) {
29
+ throw new Error("sessionId must be a valid UUID");
30
+ }
31
+ const chatInput = obj.chatInput;
32
+ if (typeof chatInput !== "string" || chatInput.length === 0) {
33
+ throw new Error("chatInput must be a non-empty string");
34
+ }
35
+ if (chatInput.length > 1e4) {
36
+ throw new Error("chatInput exceeds 10,000 character limit");
37
+ }
38
+ return { sessionId, chatInput };
39
+ }
40
+ function validateHttpsUrl(url) {
41
+ let parsed;
42
+ try {
43
+ parsed = new URL(url);
44
+ } catch {
45
+ throw new Error(`Invalid URL: ${url}`);
46
+ }
47
+ if (parsed.protocol !== "https:") {
48
+ throw new Error(`URL must be HTTPS, got: ${parsed.protocol}`);
49
+ }
50
+ return url;
51
+ }
52
+ function extractBearerToken(header) {
53
+ if (!header)
54
+ return null;
55
+ const match = header.match(/^Bearer\s+(.+)$/i);
56
+ return match?.[1] ?? null;
57
+ }
58
+
59
+ // src/response.ts
60
+ function response(options) {
61
+ if (!options.msg && options.msg !== "") {
62
+ throw new Error("msg is required");
63
+ }
64
+ const res = { msg: options.msg };
65
+ if (options.shortcut !== undefined) {
66
+ if (typeof options.shortcut !== "string" || options.shortcut.length === 0) {
67
+ throw new Error("shortcut must be a non-empty string");
68
+ }
69
+ res.shortcut = options.shortcut;
70
+ }
71
+ if (options.data !== undefined) {
72
+ res.data = options.data;
73
+ }
74
+ if (options.url !== undefined) {
75
+ res.url = validateHttpsUrl(options.url);
76
+ }
77
+ return res;
78
+ }
79
+ function responses(items) {
80
+ if (items.length === 0) {
81
+ throw new Error("At least one response is required");
82
+ }
83
+ return items.map(response);
84
+ }
85
+ function text(msg) {
86
+ return [response({ msg })];
87
+ }
88
+ function shortcut(msg, name, data) {
89
+ return [response({ msg, shortcut: name, data })];
90
+ }
91
+ function toJSON(items) {
92
+ return JSON.stringify(items);
93
+ }
94
+ function toResponse(items, status = 200) {
95
+ return new Response(toJSON(items), {
96
+ status,
97
+ headers: { "Content-Type": "application/json" }
98
+ });
99
+ }
100
+ export {
101
+ validateHttpsUrl,
102
+ toResponse,
103
+ toJSON,
104
+ text,
105
+ shortcut,
106
+ responses,
107
+ response,
108
+ parseRequest,
109
+ extractBearerToken
110
+ };
@@ -0,0 +1,57 @@
1
+ import type { PocketHookResponse, ResponseOptions } from "./types.js";
2
+ /**
3
+ * Build a single PocketHook response.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * response({ msg: "Hello!" })
8
+ * // => { msg: "Hello!" }
9
+ *
10
+ * response({ msg: "Done", shortcut: "BackupPhotos", data: { count: 42 } })
11
+ * // => { msg: "Done", shortcut: "BackupPhotos", data: { count: 42 } }
12
+ * ```
13
+ */
14
+ export declare function response(options: ResponseOptions): PocketHookResponse;
15
+ /**
16
+ * Build an array of PocketHook responses (multi-action).
17
+ * PocketHook executes shortcuts sequentially in order.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * responses([
22
+ * { msg: "Starting backup...", shortcut: "BackupPhotos" },
23
+ * { msg: "Uploading...", shortcut: "UploadToCloud", data: { dest: "s3" } }
24
+ * ])
25
+ * ```
26
+ */
27
+ export declare function responses(items: ResponseOptions[]): PocketHookResponse[];
28
+ /**
29
+ * Build a simple text-only response.
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * text("Hello from the server!")
34
+ * // => [{ msg: "Hello from the server!" }]
35
+ * ```
36
+ */
37
+ export declare function text(msg: string): PocketHookResponse[];
38
+ /**
39
+ * Build a response that triggers a Shortcut.
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * shortcut("Play Music", "PlaySpotify", { playlist: "chill" })
44
+ * // => [{ msg: "Play Music", shortcut: "PlaySpotify", data: { playlist: "chill" } }]
45
+ * ```
46
+ */
47
+ export declare function shortcut(msg: string, name: string, data?: Record<string, unknown>): PocketHookResponse[];
48
+ /**
49
+ * Serialize responses to a JSON string ready to send.
50
+ * Returns the standard PocketHook array format.
51
+ */
52
+ export declare function toJSON(items: PocketHookResponse[]): string;
53
+ /**
54
+ * Create a Response object ready to return from a server handler.
55
+ * Sets Content-Type to application/json.
56
+ */
57
+ export declare function toResponse(items: PocketHookResponse[], status?: number): Response;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * PocketHook standard request format.
3
+ * Sent as a JSON array: [PocketHookRequest]
4
+ */
5
+ export interface PocketHookRequest {
6
+ sessionId: string;
7
+ action: "sendMessage";
8
+ chatInput: string;
9
+ }
10
+ /**
11
+ * A single response action from the server.
12
+ * The server returns an array of these: PocketHookResponse[]
13
+ */
14
+ export interface PocketHookResponse {
15
+ /** Message to display in the chat. Required. */
16
+ msg: string;
17
+ /** iOS Shortcut name to trigger. Optional. */
18
+ shortcut?: string;
19
+ /** Arbitrary data payload passed to the Shortcut. Optional. */
20
+ data?: Record<string, unknown>;
21
+ /** HTTPS URL to associate with the response. Optional. Must be HTTPS. */
22
+ url?: string;
23
+ }
24
+ /**
25
+ * Server response wrapped in an "output" array.
26
+ * Alternative format: [{ output: [PocketHookResponse] }]
27
+ */
28
+ export interface PocketHookWrappedResponse {
29
+ output: PocketHookResponse[];
30
+ }
31
+ /**
32
+ * Options for the response builder.
33
+ */
34
+ export interface ResponseOptions {
35
+ /** Message to display. Required. */
36
+ msg: string;
37
+ /** Shortcut name to trigger. */
38
+ shortcut?: string;
39
+ /** Data payload for the shortcut. */
40
+ data?: Record<string, unknown>;
41
+ /** HTTPS URL. Will be validated. */
42
+ url?: string;
43
+ }
44
+ /**
45
+ * Parsed incoming request from PocketHook app.
46
+ */
47
+ export interface ParsedRequest {
48
+ sessionId: string;
49
+ chatInput: string;
50
+ }
@@ -0,0 +1,22 @@
1
+ import type { ParsedRequest } from "./types.js";
2
+ /**
3
+ * Parse and validate an incoming PocketHook request body.
4
+ * Expects a JSON array with one object: [{ sessionId, action, chatInput }]
5
+ *
6
+ * @param body - Raw request body (string, Buffer, or already parsed object)
7
+ * @returns Parsed request with sessionId and chatInput
8
+ * @throws Error if the request format is invalid
9
+ */
10
+ export declare function parseRequest(body: unknown): ParsedRequest;
11
+ /**
12
+ * Validate that a URL is HTTPS.
13
+ * PocketHook only accepts HTTPS URLs.
14
+ */
15
+ export declare function validateHttpsUrl(url: string): string;
16
+ /**
17
+ * Extract the Bearer token from an Authorization header.
18
+ *
19
+ * @param header - The Authorization header value
20
+ * @returns The token string, or null if not a Bearer token
21
+ */
22
+ export declare function extractBearerToken(header: string | null | undefined): string | null;
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "pockethook-sdk",
3
+ "version": "0.1.0",
4
+ "description": "SDK for building PocketHook-compatible server responses",
5
+ "module": "src/index.ts",
6
+ "type": "module",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "bun": "./src/index.ts",
12
+ "import": "./dist/index.js",
13
+ "types": "./dist/index.d.ts"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "src"
19
+ ],
20
+ "scripts": {
21
+ "build": "bunx --bun bun build src/index.ts --outdir dist --target node && bunx tsc -p tsconfig.build.json",
22
+ "test": "bun test",
23
+ "prepublishOnly": "bun run build"
24
+ },
25
+ "keywords": [
26
+ "pockethook",
27
+ "ios",
28
+ "shortcuts",
29
+ "automation",
30
+ "webhook",
31
+ "siri",
32
+ "n8n"
33
+ ],
34
+ "license": "MIT",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/appflowmate/pockethook-sdk.git"
38
+ },
39
+ "homepage": "https://pockethook.app",
40
+ "bugs": {
41
+ "url": "https://github.com/appflowmate/pockethook-sdk/issues"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "devDependencies": {
47
+ "@types/bun": "latest",
48
+ "typescript": "^5"
49
+ }
50
+ }
package/src/index.ts ADDED
@@ -0,0 +1,22 @@
1
+ export type {
2
+ PocketHookRequest,
3
+ PocketHookResponse,
4
+ PocketHookWrappedResponse,
5
+ ResponseOptions,
6
+ ParsedRequest,
7
+ } from "./types.js";
8
+
9
+ export {
10
+ response,
11
+ responses,
12
+ text,
13
+ shortcut,
14
+ toJSON,
15
+ toResponse,
16
+ } from "./response.js";
17
+
18
+ export {
19
+ parseRequest,
20
+ validateHttpsUrl,
21
+ extractBearerToken,
22
+ } from "./validation.js";
@@ -0,0 +1,110 @@
1
+ import type { PocketHookResponse, ResponseOptions } from "./types.js";
2
+ import { validateHttpsUrl } from "./validation.js";
3
+
4
+ /**
5
+ * Build a single PocketHook response.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * response({ msg: "Hello!" })
10
+ * // => { msg: "Hello!" }
11
+ *
12
+ * response({ msg: "Done", shortcut: "BackupPhotos", data: { count: 42 } })
13
+ * // => { msg: "Done", shortcut: "BackupPhotos", data: { count: 42 } }
14
+ * ```
15
+ */
16
+ export function response(options: ResponseOptions): PocketHookResponse {
17
+ if (!options.msg && options.msg !== "") {
18
+ throw new Error("msg is required");
19
+ }
20
+
21
+ const res: PocketHookResponse = { msg: options.msg };
22
+
23
+ if (options.shortcut !== undefined) {
24
+ if (typeof options.shortcut !== "string" || options.shortcut.length === 0) {
25
+ throw new Error("shortcut must be a non-empty string");
26
+ }
27
+ res.shortcut = options.shortcut;
28
+ }
29
+
30
+ if (options.data !== undefined) {
31
+ res.data = options.data;
32
+ }
33
+
34
+ if (options.url !== undefined) {
35
+ res.url = validateHttpsUrl(options.url);
36
+ }
37
+
38
+ return res;
39
+ }
40
+
41
+ /**
42
+ * Build an array of PocketHook responses (multi-action).
43
+ * PocketHook executes shortcuts sequentially in order.
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * responses([
48
+ * { msg: "Starting backup...", shortcut: "BackupPhotos" },
49
+ * { msg: "Uploading...", shortcut: "UploadToCloud", data: { dest: "s3" } }
50
+ * ])
51
+ * ```
52
+ */
53
+ export function responses(items: ResponseOptions[]): PocketHookResponse[] {
54
+ if (items.length === 0) {
55
+ throw new Error("At least one response is required");
56
+ }
57
+ return items.map(response);
58
+ }
59
+
60
+ /**
61
+ * Build a simple text-only response.
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * text("Hello from the server!")
66
+ * // => [{ msg: "Hello from the server!" }]
67
+ * ```
68
+ */
69
+ export function text(msg: string): PocketHookResponse[] {
70
+ return [response({ msg })];
71
+ }
72
+
73
+ /**
74
+ * Build a response that triggers a Shortcut.
75
+ *
76
+ * @example
77
+ * ```ts
78
+ * shortcut("Play Music", "PlaySpotify", { playlist: "chill" })
79
+ * // => [{ msg: "Play Music", shortcut: "PlaySpotify", data: { playlist: "chill" } }]
80
+ * ```
81
+ */
82
+ export function shortcut(
83
+ msg: string,
84
+ name: string,
85
+ data?: Record<string, unknown>
86
+ ): PocketHookResponse[] {
87
+ return [response({ msg, shortcut: name, data })];
88
+ }
89
+
90
+ /**
91
+ * Serialize responses to a JSON string ready to send.
92
+ * Returns the standard PocketHook array format.
93
+ */
94
+ export function toJSON(items: PocketHookResponse[]): string {
95
+ return JSON.stringify(items);
96
+ }
97
+
98
+ /**
99
+ * Create a Response object ready to return from a server handler.
100
+ * Sets Content-Type to application/json.
101
+ */
102
+ export function toResponse(
103
+ items: PocketHookResponse[],
104
+ status = 200
105
+ ): Response {
106
+ return new Response(toJSON(items), {
107
+ status,
108
+ headers: { "Content-Type": "application/json" },
109
+ });
110
+ }
package/src/types.ts ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * PocketHook standard request format.
3
+ * Sent as a JSON array: [PocketHookRequest]
4
+ */
5
+ export interface PocketHookRequest {
6
+ sessionId: string;
7
+ action: "sendMessage";
8
+ chatInput: string;
9
+ }
10
+
11
+ /**
12
+ * A single response action from the server.
13
+ * The server returns an array of these: PocketHookResponse[]
14
+ */
15
+ export interface PocketHookResponse {
16
+ /** Message to display in the chat. Required. */
17
+ msg: string;
18
+ /** iOS Shortcut name to trigger. Optional. */
19
+ shortcut?: string;
20
+ /** Arbitrary data payload passed to the Shortcut. Optional. */
21
+ data?: Record<string, unknown>;
22
+ /** HTTPS URL to associate with the response. Optional. Must be HTTPS. */
23
+ url?: string;
24
+ }
25
+
26
+ /**
27
+ * Server response wrapped in an "output" array.
28
+ * Alternative format: [{ output: [PocketHookResponse] }]
29
+ */
30
+ export interface PocketHookWrappedResponse {
31
+ output: PocketHookResponse[];
32
+ }
33
+
34
+ /**
35
+ * Options for the response builder.
36
+ */
37
+ export interface ResponseOptions {
38
+ /** Message to display. Required. */
39
+ msg: string;
40
+ /** Shortcut name to trigger. */
41
+ shortcut?: string;
42
+ /** Data payload for the shortcut. */
43
+ data?: Record<string, unknown>;
44
+ /** HTTPS URL. Will be validated. */
45
+ url?: string;
46
+ }
47
+
48
+ /**
49
+ * Parsed incoming request from PocketHook app.
50
+ */
51
+ export interface ParsedRequest {
52
+ sessionId: string;
53
+ chatInput: string;
54
+ }
@@ -0,0 +1,92 @@
1
+ import type { ParsedRequest } from "./types.js";
2
+
3
+ const UUID_REGEX =
4
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
5
+
6
+ /**
7
+ * Parse and validate an incoming PocketHook request body.
8
+ * Expects a JSON array with one object: [{ sessionId, action, chatInput }]
9
+ *
10
+ * @param body - Raw request body (string, Buffer, or already parsed object)
11
+ * @returns Parsed request with sessionId and chatInput
12
+ * @throws Error if the request format is invalid
13
+ */
14
+ export function parseRequest(body: unknown): ParsedRequest {
15
+ let parsed: unknown;
16
+
17
+ if (typeof body === "string") {
18
+ try {
19
+ parsed = JSON.parse(body);
20
+ } catch {
21
+ throw new Error("Invalid JSON in request body");
22
+ }
23
+ } else if (body instanceof ArrayBuffer || body instanceof Uint8Array) {
24
+ try {
25
+ const text = new TextDecoder().decode(body);
26
+ parsed = JSON.parse(text);
27
+ } catch {
28
+ throw new Error("Invalid JSON in request body");
29
+ }
30
+ } else {
31
+ parsed = body;
32
+ }
33
+
34
+ // PocketHook sends requests as an array
35
+ const arr = Array.isArray(parsed) ? parsed : [parsed];
36
+
37
+ const item = arr[0];
38
+ if (!item || typeof item !== "object") {
39
+ throw new Error("Request must contain at least one object");
40
+ }
41
+
42
+ const obj = item as Record<string, unknown>;
43
+
44
+ const sessionId = obj.sessionId;
45
+ if (typeof sessionId !== "string" || !UUID_REGEX.test(sessionId)) {
46
+ throw new Error("sessionId must be a valid UUID");
47
+ }
48
+
49
+ const chatInput = obj.chatInput;
50
+ if (typeof chatInput !== "string" || chatInput.length === 0) {
51
+ throw new Error("chatInput must be a non-empty string");
52
+ }
53
+
54
+ if (chatInput.length > 10_000) {
55
+ throw new Error("chatInput exceeds 10,000 character limit");
56
+ }
57
+
58
+ return { sessionId, chatInput };
59
+ }
60
+
61
+ /**
62
+ * Validate that a URL is HTTPS.
63
+ * PocketHook only accepts HTTPS URLs.
64
+ */
65
+ export function validateHttpsUrl(url: string): string {
66
+ let parsed: URL;
67
+ try {
68
+ parsed = new URL(url);
69
+ } catch {
70
+ throw new Error(`Invalid URL: ${url}`);
71
+ }
72
+
73
+ if (parsed.protocol !== "https:") {
74
+ throw new Error(`URL must be HTTPS, got: ${parsed.protocol}`);
75
+ }
76
+
77
+ return url;
78
+ }
79
+
80
+ /**
81
+ * Extract the Bearer token from an Authorization header.
82
+ *
83
+ * @param header - The Authorization header value
84
+ * @returns The token string, or null if not a Bearer token
85
+ */
86
+ export function extractBearerToken(
87
+ header: string | null | undefined
88
+ ): string | null {
89
+ if (!header) return null;
90
+ const match = header.match(/^Bearer\s+(.+)$/i);
91
+ return match?.[1] ?? null;
92
+ }