@khalido/home 0.0.0-stage → 0.1.1
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/.env.example +25 -0
- package/CHANGELOG.md +47 -0
- package/LICENSE +21 -0
- package/README.md +77 -2
- package/dist/clock.d.ts +17 -0
- package/dist/clock.js +43 -0
- package/dist/config.d.ts +273 -0
- package/dist/config.js +392 -0
- package/dist/connector.d.ts +34 -0
- package/dist/connector.js +47 -0
- package/dist/connectors/amber/client.d.ts +61 -0
- package/dist/connectors/amber/client.js +106 -0
- package/dist/connectors/amber/index.d.ts +30 -0
- package/dist/connectors/amber/index.js +94 -0
- package/dist/connectors/cast/codec.d.ts +38 -0
- package/dist/connectors/cast/codec.js +102 -0
- package/dist/connectors/cast/index.d.ts +60 -0
- package/dist/connectors/cast/index.js +428 -0
- package/dist/connectors/cast/schemas.d.ts +167 -0
- package/dist/connectors/cast/schemas.js +87 -0
- package/dist/connectors/cast/serve.d.ts +18 -0
- package/dist/connectors/cast/serve.js +57 -0
- package/dist/connectors/cast/state.d.ts +62 -0
- package/dist/connectors/cast/state.js +133 -0
- package/dist/connectors/cast/tls.d.ts +3 -0
- package/dist/connectors/cast/tls.js +67 -0
- package/dist/connectors/cast/writes.d.ts +80 -0
- package/dist/connectors/cast/writes.js +390 -0
- package/dist/connectors/daikin/client.d.ts +65 -0
- package/dist/connectors/daikin/client.js +221 -0
- package/dist/connectors/daikin/index.d.ts +41 -0
- package/dist/connectors/daikin/index.js +108 -0
- package/dist/connectors/enphase/client.d.ts +65 -0
- package/dist/connectors/enphase/client.js +173 -0
- package/dist/connectors/enphase/index.d.ts +43 -0
- package/dist/connectors/enphase/index.js +174 -0
- package/dist/connectors/enphase/schemas.d.ts +51 -0
- package/dist/connectors/enphase/schemas.js +34 -0
- package/dist/connectors/hue/client.d.ts +48 -0
- package/dist/connectors/hue/client.js +95 -0
- package/dist/connectors/hue/events.d.ts +41 -0
- package/dist/connectors/hue/events.js +156 -0
- package/dist/connectors/hue/index.d.ts +42 -0
- package/dist/connectors/hue/index.js +205 -0
- package/dist/connectors/hue/schemas.d.ts +65 -0
- package/dist/connectors/hue/schemas.js +48 -0
- package/dist/connectors/hue/state.d.ts +43 -0
- package/dist/connectors/hue/state.js +84 -0
- package/dist/connectors/printers/client.d.ts +28 -0
- package/dist/connectors/printers/client.js +74 -0
- package/dist/connectors/printers/codec.d.ts +76 -0
- package/dist/connectors/printers/codec.js +246 -0
- package/dist/connectors/printers/found.d.ts +15 -0
- package/dist/connectors/printers/found.js +35 -0
- package/dist/connectors/printers/index.d.ts +52 -0
- package/dist/connectors/printers/index.js +282 -0
- package/dist/connectors/printers/state.d.ts +60 -0
- package/dist/connectors/printers/state.js +99 -0
- package/dist/connectors/spotify/changes.d.ts +32 -0
- package/dist/connectors/spotify/changes.js +84 -0
- package/dist/connectors/spotify/client.d.ts +199 -0
- package/dist/connectors/spotify/client.js +270 -0
- package/dist/connectors/spotify/index.d.ts +92 -0
- package/dist/connectors/spotify/index.js +754 -0
- package/dist/connectors/up/client.d.ts +183 -0
- package/dist/connectors/up/client.js +308 -0
- package/dist/connectors/up/index.d.ts +91 -0
- package/dist/connectors/up/index.js +213 -0
- package/dist/connectors/up/spend.d.ts +78 -0
- package/dist/connectors/up/spend.js +213 -0
- package/dist/connectors/up/webhook.d.ts +13 -0
- package/dist/connectors/up/webhook.js +32 -0
- package/dist/connectors/weather/blocks.d.ts +51 -0
- package/dist/connectors/weather/blocks.js +106 -0
- package/dist/connectors/weather/bom.d.ts +37 -0
- package/dist/connectors/weather/bom.js +237 -0
- package/dist/connectors/weather/ftp.d.ts +5 -0
- package/dist/connectors/weather/ftp.js +46 -0
- package/dist/connectors/weather/google.d.ts +31 -0
- package/dist/connectors/weather/google.js +364 -0
- package/dist/connectors/weather/index.d.ts +53 -0
- package/dist/connectors/weather/index.js +280 -0
- package/dist/connectors/weather/open-meteo.d.ts +15 -0
- package/dist/connectors/weather/open-meteo.js +285 -0
- package/dist/connectors/weather/rain.d.ts +19 -0
- package/dist/connectors/weather/rain.js +51 -0
- package/dist/connectors/weather/source.d.ts +149 -0
- package/dist/connectors/weather/source.js +77 -0
- package/dist/connectors/weather/sun.d.ts +48 -0
- package/dist/connectors/weather/sun.js +65 -0
- package/dist/discover.d.ts +197 -0
- package/dist/discover.js +452 -0
- package/dist/energy.d.ts +61 -0
- package/dist/energy.js +162 -0
- package/dist/finders-entry.d.ts +1 -0
- package/dist/finders-entry.js +2 -0
- package/dist/finders.d.ts +48 -0
- package/dist/finders.js +176 -0
- package/dist/health.d.ts +28 -0
- package/dist/health.js +35 -0
- package/dist/home.d.ts +81 -0
- package/dist/home.js +114 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +11 -0
- package/dist/lane.d.ts +17 -0
- package/dist/lane.js +74 -0
- package/dist/locate.d.ts +27 -0
- package/dist/locate.js +59 -0
- package/dist/manifest.d.ts +31 -0
- package/dist/manifest.js +4 -0
- package/dist/manifests.d.ts +5 -0
- package/dist/manifests.js +23 -0
- package/dist/node-fetch.d.ts +9 -0
- package/dist/node-fetch.js +25 -0
- package/dist/poll.d.ts +10 -0
- package/dist/poll.js +33 -0
- package/docs/cast.md +133 -0
- package/docs/cli.md +41 -0
- package/docs/config.md +36 -0
- package/docs/decisions.md +150 -0
- package/docs/devices.md +128 -0
- package/docs/enphase-notes.md +24 -0
- package/docs/google.md +92 -0
- package/docs/ideas.md +69 -0
- package/docs/matter.md +55 -0
- package/docs/performance.md +12 -0
- package/docs/philosophy.md +23 -0
- package/docs/up.md +84 -0
- package/docs/using.md +65 -0
- package/docs/weather-probe.md +63 -0
- package/examples/home.jsonc +74 -0
- package/examples/home.svg +76 -0
- package/home.schema.json +474 -0
- package/llms.txt +32 -0
- package/package.json +143 -4
- package/skills/draw-home/README.md +25 -0
- package/skills/draw-home/SKILL.md +80 -0
- package/skills/use-home/SKILL.md +86 -0
- package/src/cli/cache.ts +29 -0
- package/src/cli/cast.ts +172 -0
- package/src/cli/config.ts +52 -0
- package/src/cli/context.ts +118 -0
- package/src/cli/discover.ts +182 -0
- package/src/cli/enphase.ts +62 -0
- package/src/cli/hue.ts +124 -0
- package/src/cli/locate.ts +47 -0
- package/src/cli/main.ts +182 -0
- package/src/cli/printers.ts +21 -0
- package/src/cli/probe.ts +274 -0
- package/src/cli/spotify.ts +76 -0
- package/src/cli/status.ts +228 -0
- package/src/cli/up.ts +350 -0
- package/src/clock.ts +62 -0
- package/src/config.ts +578 -0
- package/src/connector.ts +77 -0
- package/src/connectors/amber/README.md +40 -0
- package/src/connectors/amber/client.ts +157 -0
- package/src/connectors/amber/index.ts +118 -0
- package/src/connectors/cast/README.md +44 -0
- package/src/connectors/cast/codec.ts +126 -0
- package/src/connectors/cast/index.ts +552 -0
- package/src/connectors/cast/schemas.ts +101 -0
- package/src/connectors/cast/serve.ts +82 -0
- package/src/connectors/cast/state.ts +203 -0
- package/src/connectors/cast/tls.ts +100 -0
- package/src/connectors/cast/writes.ts +572 -0
- package/src/connectors/daikin/README.md +78 -0
- package/src/connectors/daikin/client.ts +295 -0
- package/src/connectors/daikin/index.ts +145 -0
- package/src/connectors/enphase/README.md +64 -0
- package/src/connectors/enphase/client.ts +244 -0
- package/src/connectors/enphase/index.ts +241 -0
- package/src/connectors/enphase/schemas.ts +45 -0
- package/src/connectors/hue/README.md +50 -0
- package/src/connectors/hue/client.ts +157 -0
- package/src/connectors/hue/events.ts +182 -0
- package/src/connectors/hue/index.ts +261 -0
- package/src/connectors/hue/schemas.ts +60 -0
- package/src/connectors/hue/state.ts +124 -0
- package/src/connectors/printers/README.md +46 -0
- package/src/connectors/printers/client.ts +114 -0
- package/src/connectors/printers/codec.ts +292 -0
- package/src/connectors/printers/found.ts +46 -0
- package/src/connectors/printers/index.ts +385 -0
- package/src/connectors/printers/state.ts +182 -0
- package/src/connectors/spotify/README.md +83 -0
- package/src/connectors/spotify/changes.ts +127 -0
- package/src/connectors/spotify/client.ts +387 -0
- package/src/connectors/spotify/index.ts +937 -0
- package/src/connectors/up/README.md +35 -0
- package/src/connectors/up/client.ts +518 -0
- package/src/connectors/up/index.ts +370 -0
- package/src/connectors/up/spend.ts +280 -0
- package/src/connectors/up/webhook.ts +62 -0
- package/src/connectors/weather/README.md +84 -0
- package/src/connectors/weather/blocks.ts +173 -0
- package/src/connectors/weather/bom.ts +344 -0
- package/src/connectors/weather/ftp.ts +48 -0
- package/src/connectors/weather/google.ts +425 -0
- package/src/connectors/weather/index.ts +396 -0
- package/src/connectors/weather/open-meteo.ts +309 -0
- package/src/connectors/weather/rain.ts +73 -0
- package/src/connectors/weather/source.ts +236 -0
- package/src/connectors/weather/sun.ts +114 -0
- package/src/discover.ts +638 -0
- package/src/energy.ts +221 -0
- package/src/finders-entry.ts +2 -0
- package/src/finders.ts +250 -0
- package/src/health.ts +74 -0
- package/src/home.ts +205 -0
- package/src/index.ts +62 -0
- package/src/lane.ts +110 -0
- package/src/locate.ts +85 -0
- package/src/manifest.ts +43 -0
- package/src/manifests.ts +27 -0
- package/src/node-fetch.ts +41 -0
- package/src/poll.ts +33 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ConnectorError } from "../../connector.ts";
|
|
3
|
+
/** A failed Up call. `status` 401 means the token is wrong or revoked; 429 the rate limit (unpublished: read `X-RateLimit-Remaining`). */
|
|
4
|
+
export declare class UpError extends ConnectorError {
|
|
5
|
+
}
|
|
6
|
+
declare const TransactionResource: z.ZodObject<{
|
|
7
|
+
id: z.ZodString;
|
|
8
|
+
attributes: z.ZodOptional<z.ZodObject<{
|
|
9
|
+
status: z.ZodOptional<z.ZodString>;
|
|
10
|
+
rawText: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
11
|
+
description: z.ZodOptional<z.ZodString>;
|
|
12
|
+
message: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
13
|
+
isCategorizable: z.ZodOptional<z.ZodBoolean>;
|
|
14
|
+
holdInfo: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
15
|
+
amount: z.ZodOptional<z.ZodObject<{
|
|
16
|
+
currencyCode: z.ZodOptional<z.ZodString>;
|
|
17
|
+
value: z.ZodOptional<z.ZodString>;
|
|
18
|
+
valueInBaseUnits: z.ZodOptional<z.ZodNumber>;
|
|
19
|
+
}, z.core.$loose>>;
|
|
20
|
+
}, z.core.$loose>>>;
|
|
21
|
+
roundUp: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
22
|
+
amount: z.ZodOptional<z.ZodObject<{
|
|
23
|
+
currencyCode: z.ZodOptional<z.ZodString>;
|
|
24
|
+
value: z.ZodOptional<z.ZodString>;
|
|
25
|
+
valueInBaseUnits: z.ZodOptional<z.ZodNumber>;
|
|
26
|
+
}, z.core.$loose>>;
|
|
27
|
+
}, z.core.$loose>>>;
|
|
28
|
+
cashback: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
29
|
+
amount: z.ZodOptional<z.ZodObject<{
|
|
30
|
+
currencyCode: z.ZodOptional<z.ZodString>;
|
|
31
|
+
value: z.ZodOptional<z.ZodString>;
|
|
32
|
+
valueInBaseUnits: z.ZodOptional<z.ZodNumber>;
|
|
33
|
+
}, z.core.$loose>>;
|
|
34
|
+
}, z.core.$loose>>>;
|
|
35
|
+
amount: z.ZodOptional<z.ZodObject<{
|
|
36
|
+
currencyCode: z.ZodOptional<z.ZodString>;
|
|
37
|
+
value: z.ZodOptional<z.ZodString>;
|
|
38
|
+
valueInBaseUnits: z.ZodOptional<z.ZodNumber>;
|
|
39
|
+
}, z.core.$loose>>;
|
|
40
|
+
foreignAmount: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
41
|
+
currencyCode: z.ZodOptional<z.ZodString>;
|
|
42
|
+
value: z.ZodOptional<z.ZodString>;
|
|
43
|
+
valueInBaseUnits: z.ZodOptional<z.ZodNumber>;
|
|
44
|
+
}, z.core.$loose>>>;
|
|
45
|
+
settledAt: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
46
|
+
createdAt: z.ZodOptional<z.ZodString>;
|
|
47
|
+
transactionType: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
48
|
+
note: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
49
|
+
text: z.ZodOptional<z.ZodString>;
|
|
50
|
+
}, z.core.$loose>>>;
|
|
51
|
+
performingCustomer: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
52
|
+
displayName: z.ZodOptional<z.ZodString>;
|
|
53
|
+
}, z.core.$loose>>>;
|
|
54
|
+
deepLinkURL: z.ZodOptional<z.ZodString>;
|
|
55
|
+
}, z.core.$loose>>;
|
|
56
|
+
relationships: z.ZodOptional<z.ZodObject<{
|
|
57
|
+
account: z.ZodOptional<z.ZodObject<{
|
|
58
|
+
data: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
59
|
+
id: z.ZodString;
|
|
60
|
+
}, z.core.$loose>>>;
|
|
61
|
+
}, z.core.$loose>>;
|
|
62
|
+
transferAccount: z.ZodOptional<z.ZodObject<{
|
|
63
|
+
data: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
64
|
+
id: z.ZodString;
|
|
65
|
+
}, z.core.$loose>>>;
|
|
66
|
+
}, z.core.$loose>>;
|
|
67
|
+
category: z.ZodOptional<z.ZodObject<{
|
|
68
|
+
data: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
69
|
+
id: z.ZodString;
|
|
70
|
+
}, z.core.$loose>>>;
|
|
71
|
+
}, z.core.$loose>>;
|
|
72
|
+
parentCategory: z.ZodOptional<z.ZodObject<{
|
|
73
|
+
data: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
74
|
+
id: z.ZodString;
|
|
75
|
+
}, z.core.$loose>>>;
|
|
76
|
+
}, z.core.$loose>>;
|
|
77
|
+
tags: z.ZodOptional<z.ZodObject<{
|
|
78
|
+
data: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
79
|
+
id: z.ZodString;
|
|
80
|
+
}, z.core.$loose>>>;
|
|
81
|
+
}, z.core.$loose>>;
|
|
82
|
+
}, z.core.$loose>>;
|
|
83
|
+
}, z.core.$loose>;
|
|
84
|
+
/** One transaction as the library keeps it: amounts in cents (Up's base units), ids flattened, nothing derived. */
|
|
85
|
+
export type Transaction = {
|
|
86
|
+
id: string;
|
|
87
|
+
status?: string;
|
|
88
|
+
description?: string;
|
|
89
|
+
rawText?: string;
|
|
90
|
+
message?: string;
|
|
91
|
+
amountCents?: number;
|
|
92
|
+
currency?: string;
|
|
93
|
+
foreignCents?: number;
|
|
94
|
+
foreignCurrency?: string;
|
|
95
|
+
heldCents?: number;
|
|
96
|
+
roundUpCents?: number;
|
|
97
|
+
cashbackCents?: number;
|
|
98
|
+
isCategorizable?: boolean;
|
|
99
|
+
createdAt?: string;
|
|
100
|
+
settledAt?: string;
|
|
101
|
+
transactionType?: string;
|
|
102
|
+
note?: string;
|
|
103
|
+
performedBy?: string;
|
|
104
|
+
deepLink?: string;
|
|
105
|
+
accountId?: string;
|
|
106
|
+
transferAccountId?: string;
|
|
107
|
+
categoryId?: string;
|
|
108
|
+
parentCategoryId?: string;
|
|
109
|
+
tags: string[];
|
|
110
|
+
};
|
|
111
|
+
export type Category = {
|
|
112
|
+
id: string;
|
|
113
|
+
name?: string;
|
|
114
|
+
parentId?: string;
|
|
115
|
+
};
|
|
116
|
+
export type Account = {
|
|
117
|
+
id: string;
|
|
118
|
+
displayName?: string;
|
|
119
|
+
accountType?: string;
|
|
120
|
+
ownershipType?: string;
|
|
121
|
+
balanceCents?: number;
|
|
122
|
+
};
|
|
123
|
+
export type Webhook = {
|
|
124
|
+
id: string;
|
|
125
|
+
url?: string;
|
|
126
|
+
description?: string;
|
|
127
|
+
createdAt?: string;
|
|
128
|
+
secretKey?: string;
|
|
129
|
+
};
|
|
130
|
+
export type WebhookLog = {
|
|
131
|
+
id: string;
|
|
132
|
+
deliveryStatus?: string;
|
|
133
|
+
createdAt?: string;
|
|
134
|
+
statusCode?: number;
|
|
135
|
+
};
|
|
136
|
+
/** Up's transaction resource to the library's shape. */
|
|
137
|
+
export declare function toTransaction(r: z.infer<typeof TransactionResource>): Transaction;
|
|
138
|
+
export type TransactionQuery = {
|
|
139
|
+
since?: string;
|
|
140
|
+
until?: string;
|
|
141
|
+
status?: "HELD" | "SETTLED";
|
|
142
|
+
category?: string;
|
|
143
|
+
tag?: string;
|
|
144
|
+
accountId?: string;
|
|
145
|
+
limit?: number;
|
|
146
|
+
pageCap?: number;
|
|
147
|
+
};
|
|
148
|
+
/** The Up client: the token comes in, nothing is stored. Reads are GETs; the writes are the three Up allows, each re-reading what Up now holds. */
|
|
149
|
+
export declare function upClient(options: {
|
|
150
|
+
token: string;
|
|
151
|
+
fetch: typeof fetch;
|
|
152
|
+
}): {
|
|
153
|
+
/** 200 when the token is good. */
|
|
154
|
+
ping(signal?: AbortSignal): Promise<void>;
|
|
155
|
+
accounts(filter?: {
|
|
156
|
+
accountType?: string;
|
|
157
|
+
ownershipType?: string;
|
|
158
|
+
}, signal?: AbortSignal): Promise<Account[]>;
|
|
159
|
+
/** Transactions newest first, following cursors. A cut-off result says so in `truncated`, so it never looks complete. */
|
|
160
|
+
transactions(query?: TransactionQuery, signal?: AbortSignal): Promise<{
|
|
161
|
+
transactions: Transaction[];
|
|
162
|
+
truncated: boolean;
|
|
163
|
+
}>;
|
|
164
|
+
transaction: (txId: string, signal?: AbortSignal) => Promise<Transaction>;
|
|
165
|
+
categories(signal?: AbortSignal): Promise<Category[]>;
|
|
166
|
+
tags(signal?: AbortSignal): Promise<string[]>;
|
|
167
|
+
/** Set a child category, or `null` to clear it. Returns the transaction as Up now holds it. */
|
|
168
|
+
setCategory(txId: string, categoryId: string | null, signal?: AbortSignal): Promise<Transaction>;
|
|
169
|
+
/** Add tags by label (at most 6 per transaction; a duplicate is ignored). Returns the transaction as Up now holds it. */
|
|
170
|
+
addTags(txId: string, labels: string[], signal?: AbortSignal): Promise<Transaction>;
|
|
171
|
+
/** Remove tags by label (an absent one is ignored). Returns the transaction as Up now holds it. */
|
|
172
|
+
removeTags(txId: string, labels: string[], signal?: AbortSignal): Promise<Transaction>;
|
|
173
|
+
webhooks(signal?: AbortSignal): Promise<Webhook[]>;
|
|
174
|
+
webhook(webhookId: string, signal?: AbortSignal): Promise<Webhook>;
|
|
175
|
+
webhookLogs(webhookId: string, signal?: AbortSignal): Promise<WebhookLog[]>;
|
|
176
|
+
/** Create a webhook. Its `secretKey` comes back this once: the caller keeps it; this client never stores or logs it. */
|
|
177
|
+
createWebhook(hookUrl: string, description?: string, signal?: AbortSignal): Promise<Webhook>;
|
|
178
|
+
deleteWebhook(webhookId: string, signal?: AbortSignal): Promise<void>;
|
|
179
|
+
/** Ask Up to send a PING event to the webhook. */
|
|
180
|
+
pingWebhook(webhookId: string, signal?: AbortSignal): Promise<void>;
|
|
181
|
+
};
|
|
182
|
+
export type UpClient = ReturnType<typeof upClient>;
|
|
183
|
+
export {};
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ConnectorError, fetchTimeout, getJson } from "../../connector.js";
|
|
3
|
+
// Up Bank's API v1 (https://developer.up.com.au/, spec github.com/up-banking/api, v1/openapi.json). Field names checked
|
|
4
|
+
// against one live read, 2026-10-16. Every field optional so a change degrades a reading rather than throwing.
|
|
5
|
+
const BASE = "https://api.up.com.au/api/v1";
|
|
6
|
+
/** A failed Up call. `status` 401 means the token is wrong or revoked; 429 the rate limit (unpublished: read `X-RateLimit-Remaining`). */
|
|
7
|
+
export class UpError extends ConnectorError {
|
|
8
|
+
}
|
|
9
|
+
const Money = z.looseObject({
|
|
10
|
+
currencyCode: z.string().optional(),
|
|
11
|
+
value: z.string().optional(),
|
|
12
|
+
valueInBaseUnits: z.number().optional(),
|
|
13
|
+
});
|
|
14
|
+
const Ref = z.looseObject({
|
|
15
|
+
data: z.looseObject({ id: z.string() }).nullable().optional(),
|
|
16
|
+
});
|
|
17
|
+
const TransactionResource = z.looseObject({
|
|
18
|
+
id: z.string(),
|
|
19
|
+
attributes: z
|
|
20
|
+
.looseObject({
|
|
21
|
+
status: z.string().optional(), // "HELD" | "SETTLED"
|
|
22
|
+
rawText: z.string().nullable().optional(),
|
|
23
|
+
description: z.string().optional(),
|
|
24
|
+
message: z.string().nullable().optional(),
|
|
25
|
+
isCategorizable: z.boolean().optional(),
|
|
26
|
+
holdInfo: z
|
|
27
|
+
.looseObject({ amount: Money.optional() })
|
|
28
|
+
.nullable()
|
|
29
|
+
.optional(),
|
|
30
|
+
roundUp: z
|
|
31
|
+
.looseObject({ amount: Money.optional() })
|
|
32
|
+
.nullable()
|
|
33
|
+
.optional(),
|
|
34
|
+
cashback: z
|
|
35
|
+
.looseObject({ amount: Money.optional() })
|
|
36
|
+
.nullable()
|
|
37
|
+
.optional(),
|
|
38
|
+
amount: Money.optional(),
|
|
39
|
+
foreignAmount: Money.nullable().optional(),
|
|
40
|
+
settledAt: z.string().nullable().optional(),
|
|
41
|
+
createdAt: z.string().optional(),
|
|
42
|
+
transactionType: z.string().nullable().optional(),
|
|
43
|
+
note: z
|
|
44
|
+
.looseObject({ text: z.string().optional() })
|
|
45
|
+
.nullable()
|
|
46
|
+
.optional(),
|
|
47
|
+
performingCustomer: z
|
|
48
|
+
.looseObject({ displayName: z.string().optional() })
|
|
49
|
+
.nullable()
|
|
50
|
+
.optional(),
|
|
51
|
+
deepLinkURL: z.string().optional(),
|
|
52
|
+
})
|
|
53
|
+
.optional(),
|
|
54
|
+
relationships: z
|
|
55
|
+
.looseObject({
|
|
56
|
+
account: Ref.optional(),
|
|
57
|
+
transferAccount: Ref.optional(),
|
|
58
|
+
category: Ref.optional(),
|
|
59
|
+
parentCategory: Ref.optional(),
|
|
60
|
+
tags: z
|
|
61
|
+
.looseObject({
|
|
62
|
+
data: z.array(z.looseObject({ id: z.string() })).optional(),
|
|
63
|
+
})
|
|
64
|
+
.optional(),
|
|
65
|
+
})
|
|
66
|
+
.optional(),
|
|
67
|
+
});
|
|
68
|
+
const Page = (item) => z.looseObject({
|
|
69
|
+
data: z.array(item),
|
|
70
|
+
links: z.looseObject({ next: z.string().nullable().optional() }).optional(),
|
|
71
|
+
});
|
|
72
|
+
const One = (item) => z.looseObject({ data: item });
|
|
73
|
+
const CategoryResource = z.looseObject({
|
|
74
|
+
id: z.string(),
|
|
75
|
+
attributes: z.looseObject({ name: z.string().optional() }).optional(),
|
|
76
|
+
relationships: z.looseObject({ parent: Ref.optional() }).optional(),
|
|
77
|
+
});
|
|
78
|
+
const AccountResource = z.looseObject({
|
|
79
|
+
id: z.string(),
|
|
80
|
+
attributes: z
|
|
81
|
+
.looseObject({
|
|
82
|
+
displayName: z.string().optional(),
|
|
83
|
+
accountType: z.string().optional(),
|
|
84
|
+
ownershipType: z.string().optional(),
|
|
85
|
+
balance: Money.optional(),
|
|
86
|
+
})
|
|
87
|
+
.optional(),
|
|
88
|
+
});
|
|
89
|
+
const WebhookResource = z.looseObject({
|
|
90
|
+
id: z.string(),
|
|
91
|
+
attributes: z
|
|
92
|
+
.looseObject({
|
|
93
|
+
url: z.string().optional(),
|
|
94
|
+
description: z.string().nullable().optional(),
|
|
95
|
+
secretKey: z.string().optional(),
|
|
96
|
+
createdAt: z.string().optional(),
|
|
97
|
+
})
|
|
98
|
+
.optional(),
|
|
99
|
+
});
|
|
100
|
+
const LogResource = z.looseObject({
|
|
101
|
+
id: z.string(),
|
|
102
|
+
attributes: z
|
|
103
|
+
.looseObject({
|
|
104
|
+
deliveryStatus: z.string().optional(),
|
|
105
|
+
createdAt: z.string().optional(),
|
|
106
|
+
response: z
|
|
107
|
+
.looseObject({ statusCode: z.number().optional() })
|
|
108
|
+
.nullable()
|
|
109
|
+
.optional(),
|
|
110
|
+
})
|
|
111
|
+
.optional(),
|
|
112
|
+
});
|
|
113
|
+
const id = (r) => r?.data?.id ?? undefined;
|
|
114
|
+
const nul = (v) => v ?? undefined;
|
|
115
|
+
/** Up's transaction resource to the library's shape. */
|
|
116
|
+
export function toTransaction(r) {
|
|
117
|
+
const a = r.attributes ?? {};
|
|
118
|
+
const rel = r.relationships ?? {};
|
|
119
|
+
return {
|
|
120
|
+
id: r.id,
|
|
121
|
+
status: a.status,
|
|
122
|
+
description: a.description,
|
|
123
|
+
rawText: nul(a.rawText),
|
|
124
|
+
message: nul(a.message),
|
|
125
|
+
amountCents: a.amount?.valueInBaseUnits,
|
|
126
|
+
currency: a.amount?.currencyCode,
|
|
127
|
+
foreignCents: nul(a.foreignAmount)?.valueInBaseUnits,
|
|
128
|
+
foreignCurrency: nul(a.foreignAmount)?.currencyCode,
|
|
129
|
+
heldCents: nul(a.holdInfo)?.amount?.valueInBaseUnits,
|
|
130
|
+
roundUpCents: nul(a.roundUp)?.amount?.valueInBaseUnits,
|
|
131
|
+
cashbackCents: nul(a.cashback)?.amount?.valueInBaseUnits,
|
|
132
|
+
isCategorizable: a.isCategorizable,
|
|
133
|
+
createdAt: a.createdAt,
|
|
134
|
+
settledAt: nul(a.settledAt),
|
|
135
|
+
transactionType: nul(a.transactionType),
|
|
136
|
+
note: nul(a.note)?.text,
|
|
137
|
+
performedBy: nul(a.performingCustomer)?.displayName,
|
|
138
|
+
deepLink: a.deepLinkURL,
|
|
139
|
+
accountId: id(rel.account),
|
|
140
|
+
transferAccountId: id(rel.transferAccount),
|
|
141
|
+
categoryId: id(rel.category),
|
|
142
|
+
parentCategoryId: id(rel.parentCategory),
|
|
143
|
+
tags: rel.tags?.data?.map((t) => t.id) ?? [],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/** The Up client: the token comes in, nothing is stored. Reads are GETs; the writes are the three Up allows, each re-reading what Up now holds. */
|
|
147
|
+
export function upClient(options) {
|
|
148
|
+
const headers = {
|
|
149
|
+
authorization: `Bearer ${options.token}`,
|
|
150
|
+
accept: "application/json",
|
|
151
|
+
};
|
|
152
|
+
const url = (path) => path.startsWith("https://") ? path : `${BASE}${path}`;
|
|
153
|
+
const get = (path, schema, signal) => getJson(options.fetch, url(path), {
|
|
154
|
+
init: { headers },
|
|
155
|
+
signal,
|
|
156
|
+
schema,
|
|
157
|
+
error: UpError,
|
|
158
|
+
});
|
|
159
|
+
/** A write: JSON in, the status checked, the body (if any) returned. Up answers 204 to category and tag writes. */
|
|
160
|
+
async function send(method, path, body, signal) {
|
|
161
|
+
const init = {
|
|
162
|
+
method,
|
|
163
|
+
headers: {
|
|
164
|
+
...headers,
|
|
165
|
+
...(body === undefined ? {} : { "content-type": "application/json" }),
|
|
166
|
+
},
|
|
167
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
168
|
+
};
|
|
169
|
+
const res = await fetchTimeout(options.fetch, url(path), init, { signal });
|
|
170
|
+
if (!res.ok)
|
|
171
|
+
throw new UpError(`HTTP ${res.status} on ${method} ${new URL(url(path)).pathname}`, { status: res.status });
|
|
172
|
+
return res.status === 204 ? undefined : res.json();
|
|
173
|
+
}
|
|
174
|
+
/** Follow `links.next` until `limit` items or `pageCap` pages; `truncated` says more were there. */
|
|
175
|
+
async function pages(path, item, limit, pageCap, signal) {
|
|
176
|
+
const out = [];
|
|
177
|
+
let next = path;
|
|
178
|
+
let n = 0;
|
|
179
|
+
while (next && out.length < limit && n < pageCap) {
|
|
180
|
+
const page = await get(next, Page(item), signal);
|
|
181
|
+
out.push(...page.data);
|
|
182
|
+
next = page.links?.next ?? undefined;
|
|
183
|
+
n++;
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
items: out.slice(0, limit),
|
|
187
|
+
truncated: next !== undefined || out.length > limit,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const transaction = async (txId, signal) => toTransaction((await get(`/transactions/${encodeURIComponent(txId)}`, One(TransactionResource), signal)).data);
|
|
191
|
+
const tagBody = (labels) => ({
|
|
192
|
+
data: labels.map((l) => ({ type: "tags", id: l })),
|
|
193
|
+
});
|
|
194
|
+
return {
|
|
195
|
+
/** 200 when the token is good. */
|
|
196
|
+
async ping(signal) {
|
|
197
|
+
await get("/util/ping", z.unknown(), signal);
|
|
198
|
+
},
|
|
199
|
+
async accounts(filter = {}, signal) {
|
|
200
|
+
const q = new URLSearchParams({ "page[size]": "100" });
|
|
201
|
+
if (filter.accountType)
|
|
202
|
+
q.set("filter[accountType]", filter.accountType);
|
|
203
|
+
if (filter.ownershipType)
|
|
204
|
+
q.set("filter[ownershipType]", filter.ownershipType);
|
|
205
|
+
const { items } = await pages(`/accounts?${q}`, AccountResource, 1000, 10, signal);
|
|
206
|
+
return items.map((r) => ({
|
|
207
|
+
id: r.id,
|
|
208
|
+
displayName: r.attributes?.displayName,
|
|
209
|
+
accountType: r.attributes?.accountType,
|
|
210
|
+
ownershipType: r.attributes?.ownershipType,
|
|
211
|
+
balanceCents: r.attributes?.balance?.valueInBaseUnits,
|
|
212
|
+
}));
|
|
213
|
+
},
|
|
214
|
+
/** Transactions newest first, following cursors. A cut-off result says so in `truncated`, so it never looks complete. */
|
|
215
|
+
async transactions(query = {}, signal) {
|
|
216
|
+
const limit = query.limit ?? 1000;
|
|
217
|
+
const q = new URLSearchParams({
|
|
218
|
+
"page[size]": String(Math.min(100, limit)),
|
|
219
|
+
});
|
|
220
|
+
for (const [k, v] of [
|
|
221
|
+
["since", query.since],
|
|
222
|
+
["until", query.until],
|
|
223
|
+
["status", query.status],
|
|
224
|
+
["category", query.category],
|
|
225
|
+
["tag", query.tag],
|
|
226
|
+
])
|
|
227
|
+
if (v)
|
|
228
|
+
q.set(`filter[${k}]`, v);
|
|
229
|
+
const base = query.accountId
|
|
230
|
+
? `/accounts/${encodeURIComponent(query.accountId)}/transactions`
|
|
231
|
+
: "/transactions";
|
|
232
|
+
const { items, truncated } = await pages(`${base}?${q}`, TransactionResource, limit, query.pageCap ?? 20, signal);
|
|
233
|
+
return { transactions: items.map(toTransaction), truncated };
|
|
234
|
+
},
|
|
235
|
+
transaction,
|
|
236
|
+
async categories(signal) {
|
|
237
|
+
const { data } = await get("/categories", z.looseObject({ data: z.array(CategoryResource) }), signal);
|
|
238
|
+
return data.map((c) => ({
|
|
239
|
+
id: c.id,
|
|
240
|
+
name: c.attributes?.name,
|
|
241
|
+
parentId: id(c.relationships?.parent),
|
|
242
|
+
}));
|
|
243
|
+
},
|
|
244
|
+
async tags(signal) {
|
|
245
|
+
return (await pages("/tags?page[size]=100", z.looseObject({ id: z.string() }), 1000, 10, signal)).items.map((t) => t.id);
|
|
246
|
+
},
|
|
247
|
+
// Writes. The integration never calls these; `home up categorise|tag` and a consumer's own code do.
|
|
248
|
+
/** Set a child category, or `null` to clear it. Returns the transaction as Up now holds it. */
|
|
249
|
+
async setCategory(txId, categoryId, signal) {
|
|
250
|
+
await send("PATCH", `/transactions/${encodeURIComponent(txId)}/relationships/category`, {
|
|
251
|
+
data: categoryId === null ? null : { type: "categories", id: categoryId },
|
|
252
|
+
}, signal);
|
|
253
|
+
return transaction(txId, signal);
|
|
254
|
+
},
|
|
255
|
+
/** Add tags by label (at most 6 per transaction; a duplicate is ignored). Returns the transaction as Up now holds it. */
|
|
256
|
+
async addTags(txId, labels, signal) {
|
|
257
|
+
await send("POST", `/transactions/${encodeURIComponent(txId)}/relationships/tags`, tagBody(labels), signal);
|
|
258
|
+
return transaction(txId, signal);
|
|
259
|
+
},
|
|
260
|
+
/** Remove tags by label (an absent one is ignored). Returns the transaction as Up now holds it. */
|
|
261
|
+
async removeTags(txId, labels, signal) {
|
|
262
|
+
await send("DELETE", `/transactions/${encodeURIComponent(txId)}/relationships/tags`, tagBody(labels), signal);
|
|
263
|
+
return transaction(txId, signal);
|
|
264
|
+
},
|
|
265
|
+
// Webhooks: reads, then writes behind the same gate.
|
|
266
|
+
async webhooks(signal) {
|
|
267
|
+
return (await pages("/webhooks?page[size]=100", WebhookResource, 1000, 10, signal)).items.map(toWebhook);
|
|
268
|
+
},
|
|
269
|
+
async webhook(webhookId, signal) {
|
|
270
|
+
return toWebhook((await get(`/webhooks/${encodeURIComponent(webhookId)}`, One(WebhookResource), signal)).data);
|
|
271
|
+
},
|
|
272
|
+
async webhookLogs(webhookId, signal) {
|
|
273
|
+
const { items } = await pages(`/webhooks/${encodeURIComponent(webhookId)}/logs?page[size]=30`, LogResource, 30, 1, signal);
|
|
274
|
+
return items.map((l) => ({
|
|
275
|
+
id: l.id,
|
|
276
|
+
deliveryStatus: l.attributes?.deliveryStatus,
|
|
277
|
+
createdAt: l.attributes?.createdAt,
|
|
278
|
+
statusCode: nul(l.attributes?.response)?.statusCode,
|
|
279
|
+
}));
|
|
280
|
+
},
|
|
281
|
+
/** Create a webhook. Its `secretKey` comes back this once: the caller keeps it; this client never stores or logs it. */
|
|
282
|
+
async createWebhook(hookUrl, description, signal) {
|
|
283
|
+
const body = await send("POST", "/webhooks", {
|
|
284
|
+
data: {
|
|
285
|
+
attributes: {
|
|
286
|
+
url: hookUrl,
|
|
287
|
+
...(description ? { description } : {}),
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
}, signal);
|
|
291
|
+
return toWebhook(One(WebhookResource).parse(body).data);
|
|
292
|
+
},
|
|
293
|
+
async deleteWebhook(webhookId, signal) {
|
|
294
|
+
await send("DELETE", `/webhooks/${encodeURIComponent(webhookId)}`, undefined, signal);
|
|
295
|
+
},
|
|
296
|
+
/** Ask Up to send a PING event to the webhook. */
|
|
297
|
+
async pingWebhook(webhookId, signal) {
|
|
298
|
+
await send("POST", `/webhooks/${encodeURIComponent(webhookId)}/ping`, undefined, signal);
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
const toWebhook = (w) => ({
|
|
303
|
+
id: w.id,
|
|
304
|
+
url: w.attributes?.url,
|
|
305
|
+
description: nul(w.attributes?.description),
|
|
306
|
+
createdAt: w.attributes?.createdAt,
|
|
307
|
+
secretKey: w.attributes?.secretKey,
|
|
308
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type Clock } from "../../clock.ts";
|
|
3
|
+
import { type Integration } from "../../home.ts";
|
|
4
|
+
import type { Manifest } from "../../manifest.ts";
|
|
5
|
+
import { type Category, type Transaction, type TransactionQuery, type UpClient } from "./client.ts";
|
|
6
|
+
import { type Household, type MerchantSpend, type SpendSummary } from "./spend.ts";
|
|
7
|
+
export { type Account, type Category, type Transaction, type TransactionQuery, type UpClient, upClient, UpError, type Webhook, type WebhookLog, } from "./client.ts";
|
|
8
|
+
export { type CategorySpend, dollars, type Household, inHousehold, isSpend, merchantKey, type MerchantSpend, notSpend, type Recurring, recurring, type SpendSummary, spendByCategory, spendByMerchant, uncategorised, type Unusual, unusual, type Window, } from "./spend.ts";
|
|
9
|
+
export { parseWebhookEvent, SIGNATURE_HEADER, verifyWebhook, type WebhookEvent, } from "./webhook.ts";
|
|
10
|
+
/** Poll every 15 minutes: spending moves slowly, and a webhook can nudge a read sooner. */
|
|
11
|
+
export declare const POLL_MS: number;
|
|
12
|
+
/** Which accounts to read: by type and ownership, never by id (e.g. the joint everyday account). All of them when left out. */
|
|
13
|
+
declare const AccountFilter: z.ZodObject<{
|
|
14
|
+
accountType: z.ZodOptional<z.ZodEnum<{
|
|
15
|
+
HOME_LOAN: "HOME_LOAN";
|
|
16
|
+
SAVER: "SAVER";
|
|
17
|
+
TRANSACTIONAL: "TRANSACTIONAL";
|
|
18
|
+
}>>;
|
|
19
|
+
ownershipType: z.ZodOptional<z.ZodEnum<{
|
|
20
|
+
INDIVIDUAL: "INDIVIDUAL";
|
|
21
|
+
JOINT: "JOINT";
|
|
22
|
+
}>>;
|
|
23
|
+
}, z.core.$strip>;
|
|
24
|
+
export type AccountFilter = z.infer<typeof AccountFilter>;
|
|
25
|
+
/** Up in a home config: the token's env name, and which accounts count as the household's. */
|
|
26
|
+
export declare const configSchema: z.ZodObject<{
|
|
27
|
+
connector: z.ZodLiteral<"up">;
|
|
28
|
+
key: z.ZodString;
|
|
29
|
+
account: z.ZodOptional<z.ZodObject<{
|
|
30
|
+
accountType: z.ZodOptional<z.ZodEnum<{
|
|
31
|
+
HOME_LOAN: "HOME_LOAN";
|
|
32
|
+
SAVER: "SAVER";
|
|
33
|
+
TRANSACTIONAL: "TRANSACTIONAL";
|
|
34
|
+
}>>;
|
|
35
|
+
ownershipType: z.ZodOptional<z.ZodEnum<{
|
|
36
|
+
INDIVIDUAL: "INDIVIDUAL";
|
|
37
|
+
JOINT: "JOINT";
|
|
38
|
+
}>>;
|
|
39
|
+
}, z.core.$strip>>;
|
|
40
|
+
household: z.ZodOptional<z.ZodObject<{
|
|
41
|
+
categories: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
42
|
+
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
43
|
+
}, z.core.$strip>>;
|
|
44
|
+
}, z.core.$loose>;
|
|
45
|
+
export type UpConfig = {
|
|
46
|
+
token: string;
|
|
47
|
+
account?: AccountFilter;
|
|
48
|
+
household?: Household;
|
|
49
|
+
fetch: typeof fetch;
|
|
50
|
+
clock?: Clock;
|
|
51
|
+
timeZone?: string;
|
|
52
|
+
};
|
|
53
|
+
/** One month's spend: Up's figures through `spendByCategory`, plus the top merchants. */
|
|
54
|
+
export type MonthSpend = SpendSummary & {
|
|
55
|
+
month: string;
|
|
56
|
+
topMerchants: MerchantSpend[];
|
|
57
|
+
};
|
|
58
|
+
/** A transaction as the snapshot keeps it: no raw text, message, note, who spent, account or balance. */
|
|
59
|
+
export type RecentTransaction = Pick<Transaction, "id" | "createdAt" | "description" | "amountCents" | "categoryId" | "parentCategoryId" | "status" | "tags">;
|
|
60
|
+
export type UpSnapshot = {
|
|
61
|
+
asOf: string;
|
|
62
|
+
thisMonth: MonthSpend;
|
|
63
|
+
lastMonth: MonthSpend;
|
|
64
|
+
uncategorised: string[];
|
|
65
|
+
recent: RecentTransaction[];
|
|
66
|
+
accounts: number;
|
|
67
|
+
household?: Household;
|
|
68
|
+
truncated: boolean;
|
|
69
|
+
};
|
|
70
|
+
/** Transactions from the accounts a filter names (every account without one), newest first. The one read the integration and `home up` share. */
|
|
71
|
+
export declare function readTransactions(client: UpClient, account: AccountFilter | undefined, query?: TransactionQuery, signal?: AbortSignal): Promise<{
|
|
72
|
+
transactions: Transaction[];
|
|
73
|
+
truncated: boolean;
|
|
74
|
+
accounts: number;
|
|
75
|
+
}>;
|
|
76
|
+
/** The snapshot from two months of transactions: pure, so tests and a consumer's own reads build the same thing. */
|
|
77
|
+
export declare function toSnapshot(txs: Transaction[], categories: Category[], at: {
|
|
78
|
+
now: number;
|
|
79
|
+
asOf: string;
|
|
80
|
+
timeZone?: string;
|
|
81
|
+
accounts: number;
|
|
82
|
+
truncated: boolean;
|
|
83
|
+
household?: Household;
|
|
84
|
+
}): UpSnapshot;
|
|
85
|
+
/** Up as a home integration: two months of transactions now, then every 15 minutes. Reads only; it never writes. */
|
|
86
|
+
export declare function up(config: UpConfig): Integration<UpSnapshot, "up">;
|
|
87
|
+
/** "Oct so far $2,140 · Sep $1,980 · 3 to categorise": this month's spend against last month's. */
|
|
88
|
+
export declare function upText(s: UpSnapshot): string;
|
|
89
|
+
/** One line per parent category this month, largest five: "home $1,030: groceries $620, utilities $410". */
|
|
90
|
+
export declare const categoryLines: (m: MonthSpend) => string[];
|
|
91
|
+
export declare const manifest: Manifest<UpSnapshot>;
|