@superdurable/connector-hackernews 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Super Durable
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 CHANGED
@@ -1,3 +1,7 @@
1
- # Temporary Holding Version
1
+ # Hacker News TypeScript connector
2
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.
3
+ The public Hacker News API connector reads feed IDs and item details with Dex Query Steps. Install `@superdurable/connector-hackernews` and declare a `hacker-news` connection named `hackernews-public` in your Dex project. The checked-in [top-story example](examples/top-story/README.md) shows the typed Flow and Worker.
4
+
5
+ `HackerNewsClient` accepts `baseUrl`, `timeoutMs`, and `maxResponseBytes`; omitted values use the manifest defaults. `newProjectConnection` reads the named Dex project connection, including the manifest `timeout` in nanoseconds. The provider is public, so no credential is required. Provider calls occur only inside Step execution. `listStoryIDs` accepts top, best, new, ask, or show and a limit from 1 to 500. `getItem` reads one positive item ID and supplies its Hacker News discussion URL. Missing, deleted, and dead items select `notFound`; rate limits and availability failures retry.
6
+
7
+ Run `npm run check` for package tests, type checking, lint, and build. Conformance fixtures replay Go-recorded provider exchanges in both TypeScript and Python. The example integration needs a running `dexcli dev` stack; live Hacker News behavior has not been verified.
@@ -0,0 +1,134 @@
1
+ {
2
+ "connectorId": "hacker-news-daily",
3
+ "releaseTag": "connectors/hackernews/go/v0.23.0",
4
+ "package": {
5
+ "ecosystem": "npm",
6
+ "name": "@superdurable/connector-hackernews",
7
+ "version": "0.2.0"
8
+ },
9
+ "factories": [
10
+ {
11
+ "name": "newListStoryIDsStep",
12
+ "kind": "query",
13
+ "operationId": "listStoryIDs",
14
+ "inputType": "ListStoryIDsInput",
15
+ "description": "List current Hacker News story IDs in feed order. Choose feed top, best, new, ask (Ask HN), or show (Show HN), and limit from 1 to 500. Read each returned ID as a Hacker News item for its title and links.",
16
+ "agent": {
17
+ "exposed": true,
18
+ "branches": {
19
+ "defect": "rejected",
20
+ "providerRejected": "rejected"
21
+ }
22
+ },
23
+ "fields": {
24
+ "stepType": "stepType",
25
+ "annotations": "annotations",
26
+ "connectionName": "connectionName",
27
+ "resultAttribute": "resultAttribute"
28
+ },
29
+ "branches": [
30
+ {
31
+ "id": "listed",
32
+ "field": "listed",
33
+ "optional": false
34
+ },
35
+ {
36
+ "id": "providerRejected",
37
+ "field": "providerRejected",
38
+ "optional": true
39
+ },
40
+ {
41
+ "id": "invalidResponse",
42
+ "field": "invalidResponse",
43
+ "optional": true
44
+ },
45
+ {
46
+ "id": "defect",
47
+ "field": "defect",
48
+ "optional": true
49
+ }
50
+ ],
51
+ "inputSchema": {
52
+ "additionalProperties": false,
53
+ "description": "ListStoryIDsInput selects one feed snapshot. It does not select a historical time window.",
54
+ "properties": {
55
+ "feed": {
56
+ "description": "Feed is top, best, new, ask, or show. Empty selects top.",
57
+ "type": "string"
58
+ },
59
+ "limit": {
60
+ "description": "Limit bounds returned IDs from 1 through 500. Zero selects 50.",
61
+ "type": "integer"
62
+ }
63
+ },
64
+ "required": [
65
+ "feed",
66
+ "limit"
67
+ ],
68
+ "type": "object"
69
+ }
70
+ },
71
+ {
72
+ "name": "newGetItemStep",
73
+ "kind": "query",
74
+ "operationId": "getItem",
75
+ "inputType": "GetItemInput",
76
+ "description": "Read one public Hacker News story, comment, job, or poll by its positive item ID. Returns title, author, score, source URL, discussion URL, text as untrusted HTML, and child comment IDs to read next.",
77
+ "agent": {
78
+ "exposed": true,
79
+ "branches": {
80
+ "defect": "rejected",
81
+ "notFound": "rejected",
82
+ "providerRejected": "rejected"
83
+ }
84
+ },
85
+ "fields": {
86
+ "stepType": "stepType",
87
+ "annotations": "annotations",
88
+ "connectionName": "connectionName",
89
+ "resultAttribute": "resultAttribute"
90
+ },
91
+ "branches": [
92
+ {
93
+ "id": "found",
94
+ "field": "found",
95
+ "optional": false
96
+ },
97
+ {
98
+ "id": "notFound",
99
+ "field": "notFound",
100
+ "optional": true
101
+ },
102
+ {
103
+ "id": "providerRejected",
104
+ "field": "providerRejected",
105
+ "optional": true
106
+ },
107
+ {
108
+ "id": "invalidResponse",
109
+ "field": "invalidResponse",
110
+ "optional": true
111
+ },
112
+ {
113
+ "id": "defect",
114
+ "field": "defect",
115
+ "optional": true
116
+ }
117
+ ],
118
+ "inputSchema": {
119
+ "additionalProperties": false,
120
+ "description": "GetItemInput identifies one story, comment, job, poll, or poll option.",
121
+ "properties": {
122
+ "id": {
123
+ "description": "ID is a positive Hacker News item ID.",
124
+ "type": "integer"
125
+ }
126
+ },
127
+ "required": [
128
+ "id"
129
+ ],
130
+ "type": "object"
131
+ }
132
+ }
133
+ ]
134
+ }
@@ -0,0 +1,165 @@
1
+ // src/generated/models.ts
2
+ import { z } from "zod";
3
+ var goTimeJSONPattern = /^(\d{4})-(\d{2})-(\d{2})T(\d{1,2}):(\d{2}):(\d{2})(?:[.,](\d+))?(Z|([+-])(\d{2}):(\d{2}))$/;
4
+ function isGoTimeJSON(value) {
5
+ const match = goTimeJSONPattern.exec(value);
6
+ if (!match) return false;
7
+ const [, yearText, monthText, dayText, hourText, minuteText, secondText, , zone, , zoneHourText, zoneMinuteText] = match;
8
+ const year = Number(yearText);
9
+ const month = Number(monthText);
10
+ const day = Number(dayText);
11
+ const isLeap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
12
+ const monthDays = [31, isLeap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
13
+ return month >= 1 && month <= 12 && day >= 1 && day <= monthDays[month - 1] && Number(hourText) <= 23 && Number(minuteText) <= 59 && Number(secondText) <= 59 && (zone === "Z" || Number(zoneHourText) <= 24 && Number(zoneMinuteText) <= 60);
14
+ }
15
+ function canonicalGoTimeJSON(value) {
16
+ const match = goTimeJSONPattern.exec(value);
17
+ const [, year, month, day, hour, minute, second, fraction, zone, sign, zoneHour, zoneMinute] = match;
18
+ const clock = year + "-" + month + "-" + day + "T" + hour.padStart(2, "0") + ":" + minute + ":" + second;
19
+ const digits = (fraction ?? "").slice(0, 9).replace(/0+$/, "");
20
+ const offsetMinutes = zone === "Z" ? 0 : (Number(zoneHour) * 60 + Number(zoneMinute)) * (sign === "+" ? 1 : -1);
21
+ if (Math.abs(offsetMinutes) >= 1440) return value;
22
+ const offset = offsetMinutes === 0 ? "Z" : (offsetMinutes > 0 ? "+" : "-") + String(Math.floor(Math.abs(offsetMinutes) / 60)).padStart(2, "0") + ":" + String(Math.abs(offsetMinutes) % 60).padStart(2, "0");
23
+ return clock + (digits ? "." + digits : "") + offset;
24
+ }
25
+ var GoTimeJSONSchema = z.string().refine(isGoTimeJSON, "invalid Go time.Time JSON timestamp").transform(canonicalGoTimeJSON);
26
+ var ConfigSchema = z.object({
27
+ "baseUrl": z.string().optional(),
28
+ "timeoutMs": z.number().int().optional(),
29
+ "maxResponseBytes": z.number().int().optional()
30
+ });
31
+ var ListStoryIDsInputSchema = z.strictObject({
32
+ "feed": z.string(),
33
+ "limit": z.number().int()
34
+ });
35
+ var StoryIDsSchema = z.strictObject({
36
+ "ids": z.array(z.number().int()).nullable(),
37
+ "hasMore": z.boolean(),
38
+ "observedAt": GoTimeJSONSchema
39
+ });
40
+ var GetItemInputSchema = z.strictObject({
41
+ "id": z.number().int()
42
+ });
43
+ var ItemSchema = z.strictObject({
44
+ "id": z.number().int(),
45
+ "type": z.string(),
46
+ "by": z.string().optional(),
47
+ "time": z.number().int().optional(),
48
+ "title": z.string().optional(),
49
+ "text": z.string().optional(),
50
+ "url": z.string().optional(),
51
+ "discussionUrl": z.string(),
52
+ "score": z.number().int().optional(),
53
+ "descendants": z.number().int().optional(),
54
+ "parent": z.number().int().optional(),
55
+ "kids": z.array(z.number().int()).optional(),
56
+ "deleted": z.boolean().optional(),
57
+ "dead": z.boolean().optional()
58
+ });
59
+
60
+ // src/generated/definitions.ts
61
+ var ConnectorID = "hacker-news-daily";
62
+ var ProviderID = "hacker-news";
63
+ var ListStoryIDsBranch = {
64
+ Listed: "listed",
65
+ ProviderRejected: "providerRejected",
66
+ InvalidResponse: "invalidResponse",
67
+ Defect: "defect"
68
+ };
69
+ var ListStoryIDsBranchListed = ListStoryIDsBranch.Listed;
70
+ var ListStoryIDsBranchProviderRejected = ListStoryIDsBranch.ProviderRejected;
71
+ var ListStoryIDsBranchInvalidResponse = ListStoryIDsBranch.InvalidResponse;
72
+ var ListStoryIDsBranchDefect = ListStoryIDsBranch.Defect;
73
+ var ListStoryIDsDefinition = {
74
+ operation: { connectorId: ConnectorID, operationId: "listStoryIDs" },
75
+ branches: [
76
+ { id: "listed", description: "The feed snapshot was read." },
77
+ { id: "providerRejected", description: "Hacker News rejected the request.", optional: true },
78
+ { id: "invalidResponse", description: "Hacker News returned malformed or oversized data.", optional: true },
79
+ { id: "defect", description: "Local input or connection configuration is invalid.", optional: true }
80
+ ],
81
+ stepDefaults: { executeMethodTimeoutMs: 3e4, executeDurability: "async", executeRetry: { initialIntervalMs: 1e3, backoffCoefficient: 2, maximumIntervalMs: 3e4, maximumAttempts: 0, totalDurationMs: 39e5 } }
82
+ };
83
+ var ListStoryIDsMetadata = { kind: "query", idempotency: "none", authorization: "none" };
84
+ var GetItemBranch = {
85
+ Found: "found",
86
+ NotFound: "notFound",
87
+ ProviderRejected: "providerRejected",
88
+ InvalidResponse: "invalidResponse",
89
+ Defect: "defect"
90
+ };
91
+ var GetItemBranchFound = GetItemBranch.Found;
92
+ var GetItemBranchNotFound = GetItemBranch.NotFound;
93
+ var GetItemBranchProviderRejected = GetItemBranch.ProviderRejected;
94
+ var GetItemBranchInvalidResponse = GetItemBranch.InvalidResponse;
95
+ var GetItemBranchDefect = GetItemBranch.Defect;
96
+ var GetItemDefinition = {
97
+ operation: { connectorId: ConnectorID, operationId: "getItem" },
98
+ branches: [
99
+ { id: "found", description: "The public item was read." },
100
+ { id: "notFound", description: "The item is missing, deleted, or dead.", optional: true },
101
+ { id: "providerRejected", description: "Hacker News rejected the request.", optional: true },
102
+ { id: "invalidResponse", description: "Hacker News returned malformed or oversized data.", optional: true },
103
+ { id: "defect", description: "Local input or connection configuration is invalid.", optional: true }
104
+ ],
105
+ stepDefaults: { executeMethodTimeoutMs: 3e4, executeDurability: "async", executeRetry: { initialIntervalMs: 1e3, backoffCoefficient: 2, maximumIntervalMs: 3e4, maximumAttempts: 0, totalDurationMs: 39e5 } }
106
+ };
107
+ var GetItemMetadata = { kind: "query", idempotency: "none", authorization: "none" };
108
+
109
+ // src/generated/connection.ts
110
+ import { ConnectionRefSchema } from "@superdurable/connectors-sdk";
111
+ var Connection = class {
112
+ /** Stable provider and connection name. */
113
+ reference;
114
+ /** Bound provider operations. */
115
+ operations;
116
+ /** Validate a reference and bind operations. */
117
+ constructor(reference, operations) {
118
+ this.reference = ConnectionRefSchema.parse(reference);
119
+ if (this.reference.provider !== ProviderID) throw new TypeError("connection provider differs from the manifest");
120
+ if (!operations) throw new TypeError("connector operations are required");
121
+ this.operations = operations;
122
+ }
123
+ };
124
+ function newConnection(client, reference) {
125
+ if (!client) throw new TypeError("connector client is required");
126
+ return new Connection(reference, client);
127
+ }
128
+ function newConnectionWithOperations(operations, reference) {
129
+ return new Connection(reference, operations);
130
+ }
131
+ function newProjectConnection(project, connectionName, createClient) {
132
+ if (!project) throw new TypeError("loaded project is required");
133
+ const reference = ConnectionRefSchema.parse({ provider: ProviderID, name: connectionName });
134
+ const selected = project.configuration.connections.find((entry) => entry.connectorId === ConnectorID && entry.connectionName === connectionName);
135
+ if (!selected || selected.provider !== ProviderID) throw new TypeError("project connection is not configured");
136
+ return newConnection(createClient(project, connectionName), reference);
137
+ }
138
+
139
+ export {
140
+ ListStoryIDsInputSchema,
141
+ StoryIDsSchema,
142
+ GetItemInputSchema,
143
+ ItemSchema,
144
+ ConnectorID,
145
+ ProviderID,
146
+ ListStoryIDsBranch,
147
+ ListStoryIDsBranchListed,
148
+ ListStoryIDsBranchProviderRejected,
149
+ ListStoryIDsBranchInvalidResponse,
150
+ ListStoryIDsBranchDefect,
151
+ ListStoryIDsDefinition,
152
+ ListStoryIDsMetadata,
153
+ GetItemBranch,
154
+ GetItemBranchFound,
155
+ GetItemBranchNotFound,
156
+ GetItemBranchProviderRejected,
157
+ GetItemBranchInvalidResponse,
158
+ GetItemBranchDefect,
159
+ GetItemDefinition,
160
+ GetItemMetadata,
161
+ Connection,
162
+ newConnection,
163
+ newConnectionWithOperations,
164
+ newProjectConnection
165
+ };
@@ -0,0 +1,100 @@
1
+ import { Query, ConnectionRef } from '@superdurable/connectors-sdk';
2
+ import { z } from 'zod';
3
+
4
+ /** Connector configuration; omitted fields use manifest defaults. */
5
+ interface Config {
6
+ /** Hacker News Firebase API base URL; this is a fixed public endpoint, so leave the field blank unless a loopback test server is being used. */
7
+ baseUrl?: string;
8
+ /** Maximum duration for one Hacker News HTTP request, using a positive Go duration such as 15s or 1m; leave blank for the displayed default. Runtime value in milliseconds. */
9
+ timeoutMs?: number;
10
+ /** Maximum bytes accepted from one Hacker News response; enter a positive integer only to change the safety limit. Oversized responses select invalidResponse. */
11
+ maxResponseBytes?: number;
12
+ }
13
+ /** Private connector credentials resolved by the host. */
14
+ interface Credentials {
15
+ }
16
+ /** ListStoryIDsInput selects one feed snapshot. It does not select a historical time window. */
17
+ interface ListStoryIDsInput {
18
+ /** Feed is top, best, new, ask, or show. Empty selects top. */
19
+ "feed": string;
20
+ /** Limit bounds returned IDs from 1 through 500. Zero selects 50. */
21
+ "limit": number;
22
+ }
23
+ /** StoryIDs contains the start of a feed in the provider's current display order. */
24
+ interface StoryIDs {
25
+ /** IDs contains at most the requested limit. Top stories can include jobs. */
26
+ "ids": Array<number> | null;
27
+ /** HasMore reports that the API returned more IDs than this operation retained. The API has no page cursor. */
28
+ "hasMore": boolean;
29
+ /** ObservedAt is the UTC time when this response was read. */
30
+ "observedAt": string;
31
+ }
32
+ /** GetItemInput identifies one story, comment, job, poll, or poll option. */
33
+ interface GetItemInput {
34
+ /** ID is a positive Hacker News item ID. */
35
+ "id": number;
36
+ }
37
+ /** Item preserves public API fields and adds the exact Hacker News permalink.
38
+ * TextHTML and Title are untrusted provider text. Applications must escape or sanitize them before HTML rendering. */
39
+ interface Item {
40
+ /** ID identifies this item. */
41
+ "id": number;
42
+ /** Type is story, comment, job, poll, or pollopt. */
43
+ "type": string;
44
+ /** By is the author's public username, when available. */
45
+ "by"?: string | undefined;
46
+ /** Time is the creation time in Unix seconds, when available. */
47
+ "time"?: number | undefined;
48
+ /** Title is the provider's title, which can contain HTML entities. */
49
+ "title"?: string | undefined;
50
+ /** TextHTML is the submitted story or comment body. Linked article content is not included. */
51
+ "text"?: string | undefined;
52
+ /** URL is the submitted external URL. Ask HN and other text posts may have no URL. */
53
+ "url"?: string | undefined;
54
+ /** DiscussionURL links directly to this story or comment on Hacker News. */
55
+ "discussionUrl": string;
56
+ /** Score is the story score. Hacker News does not expose comment scores. */
57
+ "score"?: number | undefined;
58
+ /** Descendants is the provider's comment count for a story or poll. */
59
+ "descendants"?: number | undefined;
60
+ /** Parent identifies a comment's parent item or a poll option's poll. */
61
+ "parent"?: number | undefined;
62
+ /** Kids contains child IDs in the provider's ranked display order. */
63
+ "kids"?: Array<number> | undefined;
64
+ /** Deleted reports a deleted item. GetItem selects notFound instead of returning a tombstone. */
65
+ "deleted"?: boolean | undefined;
66
+ /** Dead reports a moderated item. GetItem selects notFound instead of returning it. */
67
+ "dead"?: boolean | undefined;
68
+ }
69
+ /** Runtime validator for ListStoryIDsInput. */
70
+ declare const ListStoryIDsInputSchema: z.ZodObject<{
71
+ feed: z.ZodString;
72
+ limit: z.ZodNumber;
73
+ }, z.core.$strict>;
74
+ /** Runtime validator for GetItemInput. */
75
+ declare const GetItemInputSchema: z.ZodObject<{
76
+ id: z.ZodNumber;
77
+ }, z.core.$strict>;
78
+
79
+ /** Typed operations supplied by a client or mock. */
80
+ interface Operations {
81
+ /** Invoke listStoryIDs through this connection. */
82
+ listStoryIDs(): Query<ListStoryIDsInput, StoryIDs>;
83
+ /** Invoke getItem through this connection. */
84
+ getItem(): Query<GetItemInput, Item>;
85
+ }
86
+ /** Named connector connection kept outside Flow state. */
87
+ declare class Connection<BoundOperations = Operations> {
88
+ /** Stable provider and connection name. */
89
+ readonly reference: ConnectionRef;
90
+ /** Bound provider operations. */
91
+ readonly operations: BoundOperations;
92
+ /** Validate a reference and bind operations. */
93
+ constructor(reference: ConnectionRef, operations: BoundOperations);
94
+ }
95
+ /** Bind a client to a named connection. */
96
+ declare function newConnection(client: Operations, reference: ConnectionRef): Connection;
97
+ /** Bind scripted operations to a named connection. */
98
+ declare function newConnectionWithOperations(operations: Operations, reference: ConnectionRef): Connection;
99
+
100
+ export { Connection as C, type GetItemInput as G, type Item as I, type ListStoryIDsInput as L, type Operations as O, type StoryIDs as S, type Config as a, type Credentials as b, GetItemInputSchema as c, ListStoryIDsInputSchema as d, newConnectionWithOperations as e, newConnection as n };
@@ -0,0 +1,60 @@
1
+ import { MockQuery, Case } from '@superdurable/connectors-sdk/connectormock';
2
+ import { Failure } from '@superdurable/connectors-sdk';
3
+ import { O as Operations, C as Connection, L as ListStoryIDsInput, S as StoryIDs, G as GetItemInput, I as Item } from './connection-fC9QqgNx.js';
4
+ import 'zod';
5
+
6
+ /** Go zero value for ListStoryIDsInput in scripted cases. */
7
+ declare const emptyListStoryIDsInput: ListStoryIDsInput;
8
+ /** Go zero value for StoryIDs in scripted cases. */
9
+ declare const emptyStoryIDs: StoryIDs;
10
+ /** Go zero value for GetItemInput in scripted cases. */
11
+ declare const emptyGetItemInput: GetItemInput;
12
+ /** Go zero value for Item in scripted cases. */
13
+ declare const emptyItem: Item;
14
+ /** Script the listed branch. */
15
+ declare function listStoryIDsListed(value: StoryIDs, failure?: Failure): Case<StoryIDs>;
16
+ /** Script the providerRejected branch. */
17
+ declare function listStoryIDsProviderRejected(value: StoryIDs, failure?: Failure): Case<StoryIDs>;
18
+ /** Script the invalidResponse branch. */
19
+ declare function listStoryIDsInvalidResponse(value: StoryIDs, failure?: Failure): Case<StoryIDs>;
20
+ /** Script the defect branch. */
21
+ declare function listStoryIDsDefect(value: StoryIDs, failure?: Failure): Case<StoryIDs>;
22
+ /** Script a safe retry of listStoryIDs. */
23
+ declare function listStoryIDsRetry(failure: Failure, retryAfterMs?: number): Case<StoryIDs>;
24
+ /** Manifest scenario developerFeed for listStoryIDs. */
25
+ declare function listStoryIDsDeveloperFeed(): Case<StoryIDs>;
26
+ /** Default listStoryIDs scenario. */
27
+ declare function listStoryIDsDefault(): Case<StoryIDs>;
28
+ /** Manifest scenario feedRejected for listStoryIDs. */
29
+ declare function listStoryIDsFeedRejected(): Case<StoryIDs>;
30
+ /** Script the found branch. */
31
+ declare function getItemFound(value: Item, failure?: Failure): Case<Item>;
32
+ /** Script the notFound branch. */
33
+ declare function getItemNotFound(value: Item, failure?: Failure): Case<Item>;
34
+ /** Script the providerRejected branch. */
35
+ declare function getItemProviderRejected(value: Item, failure?: Failure): Case<Item>;
36
+ /** Script the invalidResponse branch. */
37
+ declare function getItemInvalidResponse(value: Item, failure?: Failure): Case<Item>;
38
+ /** Script the defect branch. */
39
+ declare function getItemDefect(value: Item, failure?: Failure): Case<Item>;
40
+ /** Script a safe retry of getItem. */
41
+ declare function getItemRetry(failure: Failure, retryAfterMs?: number): Case<Item>;
42
+ /** Manifest scenario compilerStory for getItem. */
43
+ declare function getItemCompilerStory(): Case<Item>;
44
+ /** Default getItem scenario. */
45
+ declare function getItemDefault(): Case<Item>;
46
+ /** Manifest scenario itemMissing for getItem. */
47
+ declare function getItemItemMissing(): Case<Item>;
48
+ /** Manifest scenario itemRejected for getItem. */
49
+ declare function getItemItemRejected(): Case<Item>;
50
+ /** Scripted operations and manifest defaults for this connector. */
51
+ declare class GeneratedMock implements Operations {
52
+ #private;
53
+ constructor(connectionName: string);
54
+ connection(): Connection;
55
+ listStoryIDs(): MockQuery<ListStoryIDsInput, StoryIDs>;
56
+ getItem(): MockQuery<GetItemInput, Item>;
57
+ assertNoFailures(): void;
58
+ }
59
+
60
+ export { GeneratedMock, emptyGetItemInput, emptyItem, emptyListStoryIDsInput, emptyStoryIDs, getItemCompilerStory, getItemDefault, getItemDefect, getItemFound, getItemInvalidResponse, getItemItemMissing, getItemItemRejected, getItemNotFound, getItemProviderRejected, getItemRetry, listStoryIDsDefault, listStoryIDsDefect, listStoryIDsDeveloperFeed, listStoryIDsFeedRejected, listStoryIDsInvalidResponse, listStoryIDsListed, listStoryIDsProviderRejected, listStoryIDsRetry };
@@ -0,0 +1,138 @@
1
+ import {
2
+ ConnectorID,
3
+ GetItemDefinition,
4
+ GetItemInputSchema,
5
+ ItemSchema,
6
+ ListStoryIDsDefinition,
7
+ ListStoryIDsInputSchema,
8
+ ProviderID,
9
+ StoryIDsSchema,
10
+ newConnectionWithOperations
11
+ } from "./chunk-23APJHSI.js";
12
+
13
+ // src/generated/mock.ts
14
+ import { branchCase, retryCase, newQuery } from "@superdurable/connectors-sdk/connectormock";
15
+ var emptyListStoryIDsInput = ListStoryIDsInputSchema.parse({
16
+ "feed": "",
17
+ "limit": 0
18
+ });
19
+ var emptyStoryIDs = StoryIDsSchema.parse({
20
+ "ids": [],
21
+ "hasMore": false,
22
+ "observedAt": "0001-01-01T00:00:00Z"
23
+ });
24
+ var emptyGetItemInput = GetItemInputSchema.parse({
25
+ "id": 0
26
+ });
27
+ var emptyItem = ItemSchema.parse({
28
+ "id": 0,
29
+ "type": "",
30
+ "discussionUrl": ""
31
+ });
32
+ function listStoryIDsListed(value, failure) {
33
+ return branchCase("listed", value, failure);
34
+ }
35
+ function listStoryIDsProviderRejected(value, failure) {
36
+ return branchCase("providerRejected", value, failure);
37
+ }
38
+ function listStoryIDsInvalidResponse(value, failure) {
39
+ return branchCase("invalidResponse", value, failure);
40
+ }
41
+ function listStoryIDsDefect(value, failure) {
42
+ return branchCase("defect", value, failure);
43
+ }
44
+ function listStoryIDsRetry(failure, retryAfterMs = 0) {
45
+ return retryCase(failure, retryAfterMs);
46
+ }
47
+ function listStoryIDsDeveloperFeed() {
48
+ const value = StoryIDsSchema.parse({ "hasMore": true, "ids": [42001], "observedAt": "2026-10-26T12:00:00Z" });
49
+ return branchCase("listed", value);
50
+ }
51
+ function listStoryIDsDefault() {
52
+ return listStoryIDsDeveloperFeed();
53
+ }
54
+ function listStoryIDsFeedRejected() {
55
+ const value = emptyStoryIDs;
56
+ return branchCase("providerRejected", value, { kind: "PROVIDER_REJECTION", provider: ConnectorID, operation: "listStoryIDs", message: "Hacker News rejected the feed request." });
57
+ }
58
+ function getItemFound(value, failure) {
59
+ return branchCase("found", value, failure);
60
+ }
61
+ function getItemNotFound(value, failure) {
62
+ return branchCase("notFound", value, failure);
63
+ }
64
+ function getItemProviderRejected(value, failure) {
65
+ return branchCase("providerRejected", value, failure);
66
+ }
67
+ function getItemInvalidResponse(value, failure) {
68
+ return branchCase("invalidResponse", value, failure);
69
+ }
70
+ function getItemDefect(value, failure) {
71
+ return branchCase("defect", value, failure);
72
+ }
73
+ function getItemRetry(failure, retryAfterMs = 0) {
74
+ return retryCase(failure, retryAfterMs);
75
+ }
76
+ function getItemCompilerStory() {
77
+ const value = ItemSchema.parse({ "by": "compilerdev", "descendants": 0, "discussionUrl": "https://news.ycombinator.com/item?id=42001", "id": 42001, "score": 100, "time": 1793016e3, "title": "A compiler release", "type": "story", "url": "https://example.com/compiler" });
78
+ return branchCase("found", value);
79
+ }
80
+ function getItemDefault() {
81
+ return getItemCompilerStory();
82
+ }
83
+ function getItemItemMissing() {
84
+ const value = emptyItem;
85
+ return branchCase("notFound", value, { kind: "NOT_FOUND", provider: ConnectorID, operation: "getItem", message: "Hacker News item is deleted or dead." });
86
+ }
87
+ function getItemItemRejected() {
88
+ const value = emptyItem;
89
+ return branchCase("providerRejected", value, { kind: "PROVIDER_REJECTION", provider: ConnectorID, operation: "getItem", message: "Hacker News rejected the item request." });
90
+ }
91
+ var GeneratedMock = class {
92
+ #listStoryIDs;
93
+ #getItem;
94
+ #connection;
95
+ constructor(connectionName) {
96
+ this.#listStoryIDs = newQuery(ListStoryIDsDefinition).default(listStoryIDsDefault());
97
+ this.#getItem = newQuery(GetItemDefinition).default(getItemDefault());
98
+ this.#connection = newConnectionWithOperations(this, { provider: ProviderID, name: connectionName });
99
+ }
100
+ connection() {
101
+ return this.#connection;
102
+ }
103
+ listStoryIDs() {
104
+ return this.#listStoryIDs;
105
+ }
106
+ getItem() {
107
+ return this.#getItem;
108
+ }
109
+ assertNoFailures() {
110
+ this.#listStoryIDs.assertNoFailures();
111
+ this.#getItem.assertNoFailures();
112
+ }
113
+ };
114
+ export {
115
+ GeneratedMock,
116
+ emptyGetItemInput,
117
+ emptyItem,
118
+ emptyListStoryIDsInput,
119
+ emptyStoryIDs,
120
+ getItemCompilerStory,
121
+ getItemDefault,
122
+ getItemDefect,
123
+ getItemFound,
124
+ getItemInvalidResponse,
125
+ getItemItemMissing,
126
+ getItemItemRejected,
127
+ getItemNotFound,
128
+ getItemProviderRejected,
129
+ getItemRetry,
130
+ listStoryIDsDefault,
131
+ listStoryIDsDefect,
132
+ listStoryIDsDeveloperFeed,
133
+ listStoryIDsFeedRejected,
134
+ listStoryIDsInvalidResponse,
135
+ listStoryIDsListed,
136
+ listStoryIDsProviderRejected,
137
+ listStoryIDsRetry
138
+ };
@@ -0,0 +1,148 @@
1
+ import { G as GetItemInput, I as Item, a as Config, L as ListStoryIDsInput, S as StoryIDs, C as Connection, O as Operations } from './connection-fC9QqgNx.js';
2
+ export { b as Credentials, c as GetItemInputSchema, d as ListStoryIDsInputSchema, n as newConnection, e as newConnectionWithOperations } from './connection-fC9QqgNx.js';
3
+ import { LoadedProject } from '@superdurable/connectors-sdk/projectconfig';
4
+ import * as _superdurable_connectors_sdk from '@superdurable/connectors-sdk';
5
+ import { Query, Call, QueryAttempt, QueryStepConfig, QueryResult, QueryStep, QueryDefinition } from '@superdurable/connectors-sdk';
6
+ import { ProviderRequest, ProviderResponse } from '@superdurable/connectors-sdk/providerhttp';
7
+ import { StepTarget } from '@superdurable/dex';
8
+ import 'zod';
9
+
10
+ /** Injectable HTTP sender and clock for deterministic provider replay. */
11
+ interface HackerNewsClientOptions {
12
+ /** Send one bounded request without redirect following. */
13
+ send?: (request: ProviderRequest) => Promise<ProviderResponse>;
14
+ /** Clock for observed feed time and Retry-After. */
15
+ now?: () => Date;
16
+ }
17
+ /** Public Hacker News API client bound to immutable configuration. */
18
+ declare class HackerNewsClient {
19
+ private readonly baseUrl;
20
+ private readonly timeoutMs;
21
+ private readonly maxResponseBytes;
22
+ private readonly send;
23
+ readonly now: () => Date;
24
+ /** Validate configuration before making requests. */
25
+ constructor(config: Config, options?: HackerNewsClientOptions);
26
+ /** Bind the feed Query. */
27
+ listStoryIDs(): ListStoryIDsOperation;
28
+ /** Bind the item Query. */
29
+ getItem(): GetItemOperation;
30
+ /** Send one read and classify its HTTP or transport outcome. */
31
+ getJSON<Output>(call: Call, operation: string, path: string, zero: Output, decode: (body: Uint8Array) => QueryAttempt<Output>): Promise<QueryAttempt<Output>>;
32
+ }
33
+ /** Read a bounded feed snapshot. */
34
+ declare class ListStoryIDsOperation implements Query<ListStoryIDsInput, StoryIDs> {
35
+ private readonly client;
36
+ /** Bind the owning client. */ constructor(client: HackerNewsClient);
37
+ /** Return manifest operation metadata. */ definition(): _superdurable_connectors_sdk.QueryDefinition;
38
+ /** Validate the feed and decode one provider response. */
39
+ invoke(call: Call, input: ListStoryIDsInput): Promise<QueryAttempt<StoryIDs>>;
40
+ }
41
+ /** Read one Hacker News story, comment, job, poll, or poll option. */
42
+ declare class GetItemOperation implements Query<GetItemInput, Item> {
43
+ private readonly client;
44
+ /** Bind the owning client. */ constructor(client: HackerNewsClient);
45
+ /** Return manifest operation metadata. */ definition(): _superdurable_connectors_sdk.QueryDefinition;
46
+ /** Validate identity and decode one item response. */
47
+ invoke(call: Call, input: GetItemInput): Promise<QueryAttempt<Item>>;
48
+ }
49
+
50
+ /** Configuration for the listStoryIDs connector Step. */
51
+ interface ListStoryIDsStepConfig<StepInput, OperationInput = ListStoryIDsInput, OperationOutput = StoryIDs> extends Omit<QueryStepConfig<StepInput, OperationInput, OperationOutput>, 'operation' | 'connection' | 'branches'> {
52
+ /** Named connection supplying this operation. */
53
+ connection: Connection<{
54
+ listStoryIDs(): Query<OperationInput, OperationOutput>;
55
+ }>;
56
+ /** Name matching the bound connection. */
57
+ connectionName: string;
58
+ /** The feed snapshot was read. */
59
+ listed: StepTarget<QueryResult<OperationOutput>>;
60
+ /** Hacker News rejected the request. */
61
+ providerRejected?: StepTarget<QueryResult<OperationOutput>>;
62
+ /** Hacker News returned malformed or oversized data. */
63
+ invalidResponse?: StepTarget<QueryResult<OperationOutput>>;
64
+ /** Local input or connection configuration is invalid. */
65
+ defect?: StepTarget<QueryResult<OperationOutput>>;
66
+ }
67
+ /** Build the listStoryIDs Step from a named connection. */
68
+ declare function newListStoryIDsStep<StepInput, OperationInput = ListStoryIDsInput, OperationOutput = StoryIDs>(configuration: ListStoryIDsStepConfig<StepInput, OperationInput, OperationOutput>): QueryStep<StepInput, OperationInput, OperationOutput>;
69
+ /** Configuration for the getItem connector Step. */
70
+ interface GetItemStepConfig<StepInput, OperationInput = GetItemInput, OperationOutput = Item> extends Omit<QueryStepConfig<StepInput, OperationInput, OperationOutput>, 'operation' | 'connection' | 'branches'> {
71
+ /** Named connection supplying this operation. */
72
+ connection: Connection<{
73
+ getItem(): Query<OperationInput, OperationOutput>;
74
+ }>;
75
+ /** Name matching the bound connection. */
76
+ connectionName: string;
77
+ /** The public item was read. */
78
+ found: StepTarget<QueryResult<OperationOutput>>;
79
+ /** The item is missing, deleted, or dead. */
80
+ notFound?: StepTarget<QueryResult<OperationOutput>>;
81
+ /** Hacker News rejected the request. */
82
+ providerRejected?: StepTarget<QueryResult<OperationOutput>>;
83
+ /** Hacker News returned malformed or oversized data. */
84
+ invalidResponse?: StepTarget<QueryResult<OperationOutput>>;
85
+ /** Local input or connection configuration is invalid. */
86
+ defect?: StepTarget<QueryResult<OperationOutput>>;
87
+ }
88
+ /** Build the getItem Step from a named connection. */
89
+ declare function newGetItemStep<StepInput, OperationInput = GetItemInput, OperationOutput = Item>(configuration: GetItemStepConfig<StepInput, OperationInput, OperationOutput>): QueryStep<StepInput, OperationInput, OperationOutput>;
90
+
91
+ /** Stable connector identity. */
92
+ declare const ConnectorID = "hacker-news-daily";
93
+ /** Provider identity for named connections. */
94
+ declare const ProviderID = "hacker-news";
95
+ /** Branch IDs for listStoryIDs. */
96
+ declare const ListStoryIDsBranch: {
97
+ readonly Listed: "listed";
98
+ readonly ProviderRejected: "providerRejected";
99
+ readonly InvalidResponse: "invalidResponse";
100
+ readonly Defect: "defect";
101
+ };
102
+ /** The feed snapshot was read. */
103
+ declare const ListStoryIDsBranchListed: "listed";
104
+ /** Hacker News rejected the request. */
105
+ declare const ListStoryIDsBranchProviderRejected: "providerRejected";
106
+ /** Hacker News returned malformed or oversized data. */
107
+ declare const ListStoryIDsBranchInvalidResponse: "invalidResponse";
108
+ /** Local input or connection configuration is invalid. */
109
+ declare const ListStoryIDsBranchDefect: "defect";
110
+ /** Manifest definition for listStoryIDs. */
111
+ declare const ListStoryIDsDefinition: QueryDefinition;
112
+ /** Manifest metadata for listStoryIDs. */
113
+ declare const ListStoryIDsMetadata: {
114
+ readonly kind: "query";
115
+ readonly idempotency: "none";
116
+ readonly authorization: "none";
117
+ };
118
+ /** Branch IDs for getItem. */
119
+ declare const GetItemBranch: {
120
+ readonly Found: "found";
121
+ readonly NotFound: "notFound";
122
+ readonly ProviderRejected: "providerRejected";
123
+ readonly InvalidResponse: "invalidResponse";
124
+ readonly Defect: "defect";
125
+ };
126
+ /** The public item was read. */
127
+ declare const GetItemBranchFound: "found";
128
+ /** The item is missing, deleted, or dead. */
129
+ declare const GetItemBranchNotFound: "notFound";
130
+ /** Hacker News rejected the request. */
131
+ declare const GetItemBranchProviderRejected: "providerRejected";
132
+ /** Hacker News returned malformed or oversized data. */
133
+ declare const GetItemBranchInvalidResponse: "invalidResponse";
134
+ /** Local input or connection configuration is invalid. */
135
+ declare const GetItemBranchDefect: "defect";
136
+ /** Manifest definition for getItem. */
137
+ declare const GetItemDefinition: QueryDefinition;
138
+ /** Manifest metadata for getItem. */
139
+ declare const GetItemMetadata: {
140
+ readonly kind: "query";
141
+ readonly idempotency: "none";
142
+ readonly authorization: "none";
143
+ };
144
+
145
+ /** Bind the named public connection and honor all project configuration fields. */
146
+ declare function newProjectConnection(project: LoadedProject, connectionName: string): Connection<Operations>;
147
+
148
+ export { Config, Connection, ConnectorID, GetItemBranch, GetItemBranchDefect, GetItemBranchFound, GetItemBranchInvalidResponse, GetItemBranchNotFound, GetItemBranchProviderRejected, GetItemDefinition, GetItemInput, GetItemMetadata, GetItemOperation, HackerNewsClient, type HackerNewsClientOptions, Item, ListStoryIDsBranch, ListStoryIDsBranchDefect, ListStoryIDsBranchInvalidResponse, ListStoryIDsBranchListed, ListStoryIDsBranchProviderRejected, ListStoryIDsDefinition, ListStoryIDsInput, ListStoryIDsMetadata, ListStoryIDsOperation, ProviderID, StoryIDs, newGetItemStep, newListStoryIDsStep, newProjectConnection };
package/dist/index.js ADDED
@@ -0,0 +1,300 @@
1
+ import {
2
+ Connection,
3
+ ConnectorID,
4
+ GetItemBranch,
5
+ GetItemBranchDefect,
6
+ GetItemBranchFound,
7
+ GetItemBranchInvalidResponse,
8
+ GetItemBranchNotFound,
9
+ GetItemBranchProviderRejected,
10
+ GetItemDefinition,
11
+ GetItemInputSchema,
12
+ GetItemMetadata,
13
+ ListStoryIDsBranch,
14
+ ListStoryIDsBranchDefect,
15
+ ListStoryIDsBranchInvalidResponse,
16
+ ListStoryIDsBranchListed,
17
+ ListStoryIDsBranchProviderRejected,
18
+ ListStoryIDsDefinition,
19
+ ListStoryIDsInputSchema,
20
+ ListStoryIDsMetadata,
21
+ ProviderID,
22
+ newConnection,
23
+ newConnectionWithOperations,
24
+ newProjectConnection
25
+ } from "./chunk-23APJHSI.js";
26
+
27
+ // src/index.ts
28
+ import { z } from "zod";
29
+
30
+ // src/client.ts
31
+ import { newQueryBranch, newQueryRetry } from "@superdurable/connectors-sdk";
32
+ import { BodyTooLargeError, ProviderStalledError, ProviderTransportError, parseRetryAfter, sendProviderRequest, validateBaseURL } from "@superdurable/connectors-sdk/providerhttp";
33
+ var defaultBaseUrl = "https://hacker-news.firebaseio.com/v0";
34
+ var zeroTime = "0001-01-01T00:00:00Z";
35
+ var zeroIds = { ids: [], hasMore: false, observedAt: zeroTime };
36
+ var zeroItem = { id: 0, type: "", discussionUrl: "" };
37
+ var validFeeds = /* @__PURE__ */ new Set(["top", "best", "new", "ask", "show"]);
38
+ var validItemTypes = /* @__PURE__ */ new Set(["story", "comment", "job", "poll", "pollopt"]);
39
+ var HackerNewsClient = class {
40
+ baseUrl;
41
+ timeoutMs;
42
+ maxResponseBytes;
43
+ send;
44
+ now;
45
+ /** Validate configuration before making requests. */
46
+ constructor(config, options = {}) {
47
+ if (!config) throw new TypeError("Hacker News configuration is required");
48
+ this.baseUrl = validateBaseURL(config.baseUrl || defaultBaseUrl);
49
+ this.timeoutMs = config.timeoutMs === void 0 || config.timeoutMs === 0 ? 15e3 : config.timeoutMs;
50
+ this.maxResponseBytes = config.maxResponseBytes === void 0 || config.maxResponseBytes === 0 ? 1048576 : config.maxResponseBytes;
51
+ if (!Number.isFinite(this.timeoutMs) || this.timeoutMs <= 0 || !Number.isSafeInteger(this.maxResponseBytes) || this.maxResponseBytes <= 0) {
52
+ throw new TypeError("Hacker News timeout and maxResponseBytes must be positive");
53
+ }
54
+ this.send = options.send ?? sendProviderRequest;
55
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
56
+ }
57
+ /** Bind the feed Query. */
58
+ listStoryIDs() {
59
+ return new ListStoryIDsOperation(this);
60
+ }
61
+ /** Bind the item Query. */
62
+ getItem() {
63
+ return new GetItemOperation(this);
64
+ }
65
+ /** Send one read and classify its HTTP or transport outcome. */
66
+ async getJSON(call, operation, path, zero, decode) {
67
+ let response;
68
+ try {
69
+ response = await this.send({
70
+ url: this.baseUrl + path,
71
+ method: "GET",
72
+ headers: { Accept: "application/json", "User-Agent": "superdurable-hacker-news-daily/0.1" },
73
+ maxResponseBytes: this.maxResponseBytes,
74
+ addressPolicy: this.baseUrl === defaultBaseUrl ? "publicHTTPS" : "privateNetworkHTTP",
75
+ timeoutMs: this.timeoutMs,
76
+ signal: call.context.cancellationSignal
77
+ });
78
+ } catch (error) {
79
+ if (error instanceof BodyTooLargeError) {
80
+ const status2 = error.status ?? 0;
81
+ if (status2 === 408 || status2 === 429 || status2 >= 500) {
82
+ const retryAfter = error.headers?.["retry-after"];
83
+ return newQueryRetry(failure(operation, status2 === 429 ? "RATE_LIMIT" : "AVAILABILITY", "Hacker News is temporarily unavailable"), parseRetryAfter(Array.isArray(retryAfter) ? retryAfter[0] ?? "" : retryAfter ?? "", this.now()));
84
+ }
85
+ if (status2 === 404 && operation === "getItem") return failed(operation, "notFound", zero, "NOT_FOUND", "Hacker News resource was not found");
86
+ if (status2 !== 200) return failed(operation, "providerRejected", zero, "PROVIDER_REJECTION", "Hacker News rejected the request");
87
+ return failed(operation, "invalidResponse", zero, "RESPONSE_TOO_LARGE", "Hacker News response exceeded the configured limit");
88
+ }
89
+ const isInterruptedResponse = (error instanceof ProviderTransportError || error instanceof ProviderStalledError) && error.status === 200;
90
+ return newQueryRetry(failure(operation, "AVAILABILITY", isInterruptedResponse ? "Hacker News response was interrupted" : "Hacker News request failed"), 0);
91
+ }
92
+ const status = response.status;
93
+ if (status === 408 || status === 429 || status >= 500) {
94
+ const retryAfter = response.headers["retry-after"];
95
+ return newQueryRetry(failure(operation, status === 429 ? "RATE_LIMIT" : "AVAILABILITY", "Hacker News is temporarily unavailable"), parseRetryAfter(Array.isArray(retryAfter) ? retryAfter[0] ?? "" : retryAfter ?? "", this.now()));
96
+ }
97
+ if (status === 404 && operation === "getItem") return failed(operation, "notFound", zero, "NOT_FOUND", "Hacker News resource was not found");
98
+ if (status !== 200) return failed(operation, "providerRejected", zero, "PROVIDER_REJECTION", "Hacker News rejected the request");
99
+ return decode(response.body);
100
+ }
101
+ };
102
+ var ListStoryIDsOperation = class {
103
+ /** Bind the owning client. */
104
+ constructor(client) {
105
+ this.client = client;
106
+ }
107
+ client;
108
+ /** Return manifest operation metadata. */
109
+ definition() {
110
+ return ListStoryIDsDefinition;
111
+ }
112
+ /** Validate the feed and decode one provider response. */
113
+ invoke(call, input) {
114
+ const feed = input.feed || "top";
115
+ const limit = input.limit || 50;
116
+ if (!validFeeds.has(feed)) return Promise.resolve(failed("listStoryIDs", "defect", zeroIds, "VALIDATION", "feed must be top, best, new, ask, or show"));
117
+ if (!Number.isInteger(limit) || limit < 1 || limit > 500) return Promise.resolve(failed("listStoryIDs", "defect", zeroIds, "VALIDATION", "limit must be from 1 through 500"));
118
+ return this.client.getJSON(call, "listStoryIDs", `/${feed}stories.json`, zeroIds, (body) => {
119
+ let ids;
120
+ try {
121
+ ids = parseProviderJSON(body);
122
+ } catch {
123
+ ids = void 0;
124
+ }
125
+ if (!Array.isArray(ids)) return failed("listStoryIDs", "invalidResponse", zeroIds, "PROTOCOL", "Hacker News returned an invalid feed");
126
+ const feedIDs = [];
127
+ for (const id of ids) {
128
+ if (id === null) return failed("listStoryIDs", "invalidResponse", zeroIds, "PROTOCOL", "Hacker News returned an invalid item ID");
129
+ try {
130
+ feedIDs.push(strictInteger(id, "item ID"));
131
+ } catch (error) {
132
+ return failed("listStoryIDs", "invalidResponse", zeroIds, "PROTOCOL", `Hacker News returned an invalid feed: ${fieldError(error)}`);
133
+ }
134
+ }
135
+ if (feedIDs.some((id) => id <= 0)) return failed("listStoryIDs", "invalidResponse", zeroIds, "PROTOCOL", "Hacker News returned an invalid item ID");
136
+ return newQueryBranch("listed", { ids: feedIDs.slice(0, limit), hasMore: feedIDs.length > limit, observedAt: goTimestamp(this.client.now()) }, {});
137
+ });
138
+ }
139
+ };
140
+ var GetItemOperation = class {
141
+ /** Bind the owning client. */
142
+ constructor(client) {
143
+ this.client = client;
144
+ }
145
+ client;
146
+ /** Return manifest operation metadata. */
147
+ definition() {
148
+ return GetItemDefinition;
149
+ }
150
+ /** Validate identity and decode one item response. */
151
+ invoke(call, input) {
152
+ if (!Number.isSafeInteger(input.id) || input.id <= 0) return Promise.resolve(failed("getItem", "defect", zeroItem, "VALIDATION", "item ID must be positive"));
153
+ return this.client.getJSON(call, "getItem", `/item/${input.id}.json`, zeroItem, (body) => {
154
+ let value;
155
+ try {
156
+ value = parseProviderJSON(body);
157
+ } catch {
158
+ return failed("getItem", "invalidResponse", zeroItem, "PROTOCOL", "Hacker News returned malformed item JSON");
159
+ }
160
+ if (value === null) return failed("getItem", "notFound", zeroItem, "NOT_FOUND", "Hacker News item is deleted or dead");
161
+ let item;
162
+ try {
163
+ item = decodeItem(value);
164
+ } catch (error) {
165
+ return failed("getItem", "invalidResponse", zeroItem, "PROTOCOL", `Hacker News returned malformed item JSON: ${fieldError(error)}`);
166
+ }
167
+ if (item.deleted || item.dead) return failed("getItem", "notFound", zeroItem, "NOT_FOUND", "Hacker News item is deleted or dead");
168
+ if (item.id !== input.id) return failed("getItem", "invalidResponse", zeroItem, "PROTOCOL", "Hacker News returned a different item ID");
169
+ if (!validItemTypes.has(item.type)) return failed("getItem", "invalidResponse", zeroItem, "PROTOCOL", "Hacker News returned an unknown item type");
170
+ if (item.kids?.some((id) => id <= 0)) return failed("getItem", "invalidResponse", zeroItem, "PROTOCOL", "Hacker News returned an invalid child ID");
171
+ return newQueryBranch("found", item, {});
172
+ });
173
+ }
174
+ };
175
+ function decodeItem(value) {
176
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError("item must be an object");
177
+ const document = value;
178
+ const string = (name) => {
179
+ const field = document[name];
180
+ if (field === void 0 || field === null) return "";
181
+ if (typeof field !== "string") throw new TypeError(name);
182
+ return field;
183
+ };
184
+ const integer = (name) => {
185
+ const field = document[name];
186
+ if (field === void 0 || field === null) return 0;
187
+ return strictInteger(field, name);
188
+ };
189
+ const boolean = (name) => {
190
+ const field = document[name];
191
+ if (field === void 0 || field === null) return false;
192
+ if (typeof field !== "boolean") throw new TypeError(name);
193
+ return field;
194
+ };
195
+ const kids = document.kids;
196
+ if (kids !== void 0 && kids !== null && !Array.isArray(kids)) throw new TypeError("kids");
197
+ const childIDs = Array.isArray(kids) ? kids.map((id2) => id2 === null ? 0 : strictInteger(id2, "kids")) : [];
198
+ const id = integer("id");
199
+ const item = { id, type: string("type"), discussionUrl: `https://news.ycombinator.com/item?id=${id}` };
200
+ for (const name of ["by", "title", "text", "url"]) {
201
+ const field = string(name);
202
+ if (field) item[name] = field;
203
+ }
204
+ for (const name of ["time", "score", "descendants", "parent"]) {
205
+ const field = integer(name);
206
+ if (field) item[name] = field;
207
+ }
208
+ if (childIDs.length) item.kids = childIDs;
209
+ if (boolean("deleted")) item.deleted = true;
210
+ if (boolean("dead")) item.dead = true;
211
+ return item;
212
+ }
213
+ function parseProviderJSON(body) {
214
+ const exactJSON = JSON;
215
+ return exactJSON.parse(Buffer.from(body).toString("utf8"), (_key, value, context) => typeof value === "number" && context?.source ? exactJSON.rawJSON(context.source) : value);
216
+ }
217
+ function strictInteger(value, name) {
218
+ const source = value && typeof value === "object" && "rawJSON" in value ? String(value.rawJSON) : "";
219
+ if (!/^-?(?:0|[1-9]\d*)$/.test(source)) throw new TypeError(name);
220
+ const integer = Number(source);
221
+ if (!Number.isSafeInteger(integer)) throw new TypeError(name);
222
+ return integer;
223
+ }
224
+ function fieldError(error) {
225
+ return error instanceof Error ? error.message : "unknown field";
226
+ }
227
+ function goTimestamp(time) {
228
+ return time.toISOString().replace(/\.000Z$/, "Z").replace(/(\.\d*?[1-9])0+Z$/, "$1Z");
229
+ }
230
+ function failure(operation, kind, message) {
231
+ return { kind, provider: "hacker-news", operation, message };
232
+ }
233
+ function failed(operation, branch, zero, kind, message) {
234
+ return newQueryBranch(branch, zero, {}, failure(operation, kind, message));
235
+ }
236
+
237
+ // src/generated/steps.ts
238
+ import { goToBranch, newQueryStep } from "@superdurable/connectors-sdk";
239
+ function newListStoryIDsStep(configuration) {
240
+ const { connection, connectionName, listed, providerRejected, invalidResponse, defect, ...stepConfiguration } = configuration;
241
+ if (!connection || connection.reference.name !== connectionName) throw new TypeError("Step connection name differs from the bound connection");
242
+ return newQueryStep({ ...stepConfiguration, operation: connection.operations.listStoryIDs(), connection: connection.reference, branches: [
243
+ goToBranch("listed", listed),
244
+ ...providerRejected ? [goToBranch("providerRejected", providerRejected)] : [],
245
+ ...invalidResponse ? [goToBranch("invalidResponse", invalidResponse)] : [],
246
+ ...defect ? [goToBranch("defect", defect)] : []
247
+ ] });
248
+ }
249
+ function newGetItemStep(configuration) {
250
+ const { connection, connectionName, found, notFound, providerRejected, invalidResponse, defect, ...stepConfiguration } = configuration;
251
+ if (!connection || connection.reference.name !== connectionName) throw new TypeError("Step connection name differs from the bound connection");
252
+ return newQueryStep({ ...stepConfiguration, operation: connection.operations.getItem(), connection: connection.reference, branches: [
253
+ goToBranch("found", found),
254
+ ...notFound ? [goToBranch("notFound", notFound)] : [],
255
+ ...providerRejected ? [goToBranch("providerRejected", providerRejected)] : [],
256
+ ...invalidResponse ? [goToBranch("invalidResponse", invalidResponse)] : [],
257
+ ...defect ? [goToBranch("defect", defect)] : []
258
+ ] });
259
+ }
260
+
261
+ // src/index.ts
262
+ function newProjectConnection2(project, connectionName) {
263
+ return newProjectConnection(project, connectionName, (loadedProject, name) => {
264
+ const selected = loadedProject.configuration.connections.find((entry) => entry.connectorId === "hacker-news-daily" && entry.connectionName === name);
265
+ if (!selected) throw new TypeError("Hacker News project connection is not configured");
266
+ const raw = z.strictObject({ baseUrl: z.string().optional(), timeout: z.number().int().refine(Number.isSafeInteger).optional(), maxResponseBytes: z.number().int().refine(Number.isSafeInteger).optional() }).parse(selected.configuration);
267
+ const config = { baseUrl: raw.baseUrl, timeoutMs: raw.timeout === void 0 ? void 0 : raw.timeout / 1e6, maxResponseBytes: raw.maxResponseBytes };
268
+ return new HackerNewsClient(config);
269
+ });
270
+ }
271
+ export {
272
+ Connection,
273
+ ConnectorID,
274
+ GetItemBranch,
275
+ GetItemBranchDefect,
276
+ GetItemBranchFound,
277
+ GetItemBranchInvalidResponse,
278
+ GetItemBranchNotFound,
279
+ GetItemBranchProviderRejected,
280
+ GetItemDefinition,
281
+ GetItemInputSchema,
282
+ GetItemMetadata,
283
+ GetItemOperation,
284
+ HackerNewsClient,
285
+ ListStoryIDsBranch,
286
+ ListStoryIDsBranchDefect,
287
+ ListStoryIDsBranchInvalidResponse,
288
+ ListStoryIDsBranchListed,
289
+ ListStoryIDsBranchProviderRejected,
290
+ ListStoryIDsDefinition,
291
+ ListStoryIDsInputSchema,
292
+ ListStoryIDsMetadata,
293
+ ListStoryIDsOperation,
294
+ ProviderID,
295
+ newConnection,
296
+ newConnectionWithOperations,
297
+ newGetItemStep,
298
+ newListStoryIDsStep,
299
+ newProjectConnection2 as newProjectConnection
300
+ };
package/package.json CHANGED
@@ -1,6 +1,60 @@
1
1
  {
2
2
  "name": "@superdurable/connector-hackernews",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "license": "MIT",
5
+ "description": "Hacker News connector for Dex TypeScript applications",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/superdurable/dex-connectors-library.git",
9
+ "directory": "connectors/hackernews/typescript"
10
+ },
11
+ "type": "module",
12
+ "engines": {
13
+ "node": "^22.0.0 || ^24.0.0"
14
+ },
15
+ "module": "./dist/index.js",
16
+ "types": "./dist/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js"
21
+ },
22
+ "./hackernewsmock": {
23
+ "types": "./dist/hackernewsmock.d.ts",
24
+ "import": "./dist/hackernewsmock.js"
25
+ },
26
+ "./dex-connector.json": "./dex-connector.json"
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "README.md",
31
+ "LICENSE",
32
+ "dex-connector.json"
33
+ ],
34
+ "scripts": {
35
+ "prepack": "npm run build",
36
+ "build": "tsup src/index.ts src/hackernewsmock.ts --format esm --dts --clean",
37
+ "typecheck": "tsc --noEmit",
38
+ "lint": "eslint src test examples",
39
+ "test": "vitest run",
40
+ "check": "npm run typecheck && npm run lint && npm test && npm run build",
41
+ "example:top-story": "tsx examples/top-story/worker.ts",
42
+ "integration:top-story": "tsx examples/top-story/integration.ts"
43
+ },
44
+ "dependencies": {
45
+ "@superdurable/connectors-sdk": "0.8.0",
46
+ "@superdurable/dex": ">=1.28.0",
47
+ "zod": "^4.1.0"
48
+ },
49
+ "devDependencies": {
50
+ "@eslint/js": "^9.0.0",
51
+ "@types/node": "^22.0.0",
52
+ "eslint": "^9.0.0",
53
+ "tsup": "^8.0.0",
54
+ "tsx": "^4.23.15",
55
+ "typescript": "^5.9.0",
56
+ "typescript-eslint": "^8.0.0",
57
+ "vitest": "^3.0.0",
58
+ "yaml": "^2.9.1"
59
+ }
60
+ }