@guinetik/primitives-ts 0.14.0 → 0.15.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.
@@ -1,5 +1,5 @@
1
- import type { GeoLocation } from './geo.types';
2
- export type HomeWidgetType = "launchpad" | "clock" | "webpage" | "analytics" | "utility" | "news" | "weather" | "currency";
1
+ import type { GeoLocation } from "./geo.types";
2
+ export type HomeWidgetType = "launchpad" | "clock" | "webpage" | "analytics" | "utility" | "news" | "weather" | "currency" | "crypto" | "apod";
3
3
  export type HomeWidgetColSpan = 1 | 2 | 3;
4
4
  export interface LaunchpadWidgetConfig {
5
5
  /** Launchpad group ids to show. Omitted on legacy widgets means all groups. */
@@ -51,7 +51,20 @@ export interface CurrencyWidgetConfig {
51
51
  quote: string;
52
52
  defaultAmount: number;
53
53
  }
54
- export type HomeWidgetConfig = LaunchpadWidgetConfig | ClockWidgetConfig | WebpageWidgetConfig | AnalyticsWidgetConfig | UtilityWidgetConfig | NewsWidgetConfig | WeatherWidgetConfig | CurrencyWidgetConfig;
54
+ export declare const CRYPTO_MAX_COINS = 5;
55
+ export interface CryptoWidgetConfig {
56
+ /** Enabled provider to use. Omitted means the category default. */
57
+ providerId?: string;
58
+ /** CoinGecko-style coin ids, e.g. "bitcoin". */
59
+ coins: string[];
60
+ /** Uppercase ISO 4217 code the coins are priced in, e.g. "BRL". */
61
+ vsCurrency: string;
62
+ }
63
+ export interface ApodWidgetConfig {
64
+ /** Enabled NASA provider to use. Omitted means the NASA category default. */
65
+ providerId?: string;
66
+ }
67
+ export type HomeWidgetConfig = LaunchpadWidgetConfig | ClockWidgetConfig | WebpageWidgetConfig | AnalyticsWidgetConfig | UtilityWidgetConfig | NewsWidgetConfig | WeatherWidgetConfig | CurrencyWidgetConfig | CryptoWidgetConfig | ApodWidgetConfig;
55
68
  export interface HomeDashboardWidget {
56
69
  id: string;
57
70
  /** Optional user-defined chrome title. The widget type is used when omitted. */
@@ -71,4 +84,6 @@ export declare const CLOCK_DEFAULT_TIMEZONE = "America/Sao_Paulo";
71
84
  export declare const CLOCK_DEFAULT_PRESET = "aurora";
72
85
  export declare const CLOCK_PRESETS: readonly ["aurora", "grid", "binary", "day-night", "location-photos"];
73
86
  export declare function isHomeWidgetType(value: string): value is HomeWidgetType;
87
+ export type HomeWidgetConfigValidationResult = string | null;
88
+ export declare function validateHomeWidgetConfig(type: HomeWidgetType, config: unknown): HomeWidgetConfigValidationResult;
74
89
  //# sourceMappingURL=home-dashboard.types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"home-dashboard.types.d.ts","sourceRoot":"","sources":["../src/home-dashboard.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,MAAM,MAAM,cAAc,GACtB,WAAW,GACX,OAAO,GACP,SAAS,GACT,WAAW,GACX,SAAS,GACT,MAAM,GACN,SAAS,GACT,UAAU,CAAC;AAEf,MAAM,MAAM,iBAAiB,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAE1C,MAAM,WAAW,qBAAqB;IACpC,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,4EAA4E;IAC5E,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,yDAAyD;IACzD,QAAQ,CAAC,EAAE,WAAW,CAAC;CACxB;AAED,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,qBAAqB;IACpC,kEAAkE;IAClE,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,gBAAgB,kEAInB,CAAC;AAEX,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE9D;;;GAGG;AACH,MAAM,WAAW,mBAAmB;IAClC,aAAa,EAAE,CAAC,CAAC;IACjB,MAAM,EAAE,aAAa,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,gBAAgB;IAC/B,mEAAmE;IACnE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,mBAAmB;IAClC,mEAAmE;IACnE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sDAAsD;IACtD,QAAQ,EAAE,MAAM,GAAG,WAAW,CAAC;CAChC;AAED,MAAM,WAAW,oBAAoB;IACnC,mEAAmE;IACnE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,MAAM,gBAAgB,GACxB,qBAAqB,GACrB,iBAAiB,GACjB,mBAAmB,GACnB,qBAAqB,GACrB,mBAAmB,GACnB,gBAAgB,GAChB,mBAAmB,GACnB,oBAAoB,CAAC;AAEzB,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,gFAAgF;IAChF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,cAAc,CAAC;IACrB,OAAO,EAAE,iBAAiB,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,gBAAgB,CAAC;CAC1B;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,CAAC,CAAC;IACX,OAAO,EAAE,mBAAmB,EAAE,CAAC;CAChC;AAED,eAAO,MAAM,iBAAiB,EAAE,SAAS,cAAc,EAStD,CAAC;AAEF,eAAO,MAAM,gBAAgB,KAAK,CAAC;AAEnC,eAAO,MAAM,sBAAsB,sBAAsB,CAAC;AAE1D,eAAO,MAAM,oBAAoB,WAAW,CAAC;AAE7C,eAAO,MAAM,aAAa,uEAMhB,CAAC;AAEX,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,cAAc,CAEvE"}
1
+ {"version":3,"file":"home-dashboard.types.d.ts","sourceRoot":"","sources":["../src/home-dashboard.types.ts"],"names":[],"mappings":"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"}
@@ -1,12 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CLOCK_PRESETS = exports.CLOCK_DEFAULT_PRESET = exports.CLOCK_DEFAULT_TIMEZONE = exports.MAX_HOME_WIDGETS = exports.HOME_WIDGET_TYPES = exports.UTILITY_TOOL_IDS = void 0;
3
+ exports.CLOCK_PRESETS = exports.CLOCK_DEFAULT_PRESET = exports.CLOCK_DEFAULT_TIMEZONE = exports.MAX_HOME_WIDGETS = exports.HOME_WIDGET_TYPES = exports.CRYPTO_MAX_COINS = exports.UTILITY_TOOL_IDS = void 0;
4
4
  exports.isHomeWidgetType = isHomeWidgetType;
5
+ exports.validateHomeWidgetConfig = validateHomeWidgetConfig;
5
6
  exports.UTILITY_TOOL_IDS = [
6
7
  "unit-converter",
7
8
  "text-workbench",
8
9
  "date-calculator",
9
10
  ];
11
+ exports.CRYPTO_MAX_COINS = 5;
10
12
  exports.HOME_WIDGET_TYPES = [
11
13
  "launchpad",
12
14
  "clock",
@@ -16,6 +18,8 @@ exports.HOME_WIDGET_TYPES = [
16
18
  "news",
17
19
  "weather",
18
20
  "currency",
21
+ "crypto",
22
+ "apod",
19
23
  ];
20
24
  exports.MAX_HOME_WIDGETS = 30;
21
25
  exports.CLOCK_DEFAULT_TIMEZONE = "America/Sao_Paulo";
@@ -30,3 +34,193 @@ exports.CLOCK_PRESETS = [
30
34
  function isHomeWidgetType(value) {
31
35
  return exports.HOME_WIDGET_TYPES.includes(value);
32
36
  }
37
+ function configRecord(config) {
38
+ return typeof config === "object" && config !== null && !Array.isArray(config)
39
+ ? config
40
+ : null;
41
+ }
42
+ function providerIdError(config) {
43
+ const providerId = config.providerId;
44
+ return providerId !== undefined &&
45
+ (typeof providerId !== "string" ||
46
+ providerId.trim().length === 0 ||
47
+ providerId.length > 200)
48
+ ? "widget providerId must be a non-empty string"
49
+ : null;
50
+ }
51
+ const HOME_WIDGET_CONFIG_VALIDATORS = {
52
+ launchpad(config) {
53
+ const value = configRecord(config);
54
+ if (!value)
55
+ return "launchpad widget config must be an object";
56
+ const ids = value.groupIds;
57
+ if (ids !== undefined &&
58
+ (!Array.isArray(ids) ||
59
+ ids.length === 0 ||
60
+ ids.some((id) => typeof id !== "string" || id.trim().length === 0) ||
61
+ new Set(ids).size !== ids.length)) {
62
+ return "launchpad widget config groupIds must be a non-empty array of unique strings";
63
+ }
64
+ return null;
65
+ },
66
+ clock(config) {
67
+ const value = configRecord(config);
68
+ if (!value ||
69
+ typeof value.timeZone !== "string" ||
70
+ value.timeZone.length === 0 ||
71
+ typeof value.preset !== "string" ||
72
+ value.preset.length === 0) {
73
+ return "clock widget config requires timeZone and preset";
74
+ }
75
+ return value.hour12 !== undefined && typeof value.hour12 !== "boolean"
76
+ ? "clock widget hour12 must be a boolean"
77
+ : null;
78
+ },
79
+ webpage(config) {
80
+ const value = configRecord(config);
81
+ if (!value || typeof value.url !== "string") {
82
+ return "webpage widget config url must be a string";
83
+ }
84
+ return value.url.length > 0 && !/^https?:\/\//i.test(value.url)
85
+ ? "webpage widget config url must start with http:// or https://"
86
+ : null;
87
+ },
88
+ analytics(config) {
89
+ const propertyId = configRecord(config)?.propertyId;
90
+ return typeof propertyId !== "string" ||
91
+ !/^properties\/\d+$/.test(propertyId)
92
+ ? "analytics widget config propertyId must look like properties/123456"
93
+ : null;
94
+ },
95
+ utility(config) {
96
+ const value = configRecord(config);
97
+ if (!value)
98
+ return "utility widget config must be an object";
99
+ if (value.schemaVersion !== 1)
100
+ return "utility widget config schemaVersion must be 1";
101
+ if (typeof value.toolId !== "string" ||
102
+ !exports.UTILITY_TOOL_IDS.includes(value.toolId)) {
103
+ return "utility widget config contains an unsupported toolId";
104
+ }
105
+ const settings = configRecord(value.settings);
106
+ if (!settings)
107
+ return "utility widget settings must be an object";
108
+ return new TextEncoder().encode(JSON.stringify(settings)).length > 4096
109
+ ? "utility widget settings are too large"
110
+ : null;
111
+ },
112
+ news(config) {
113
+ const value = configRecord(config);
114
+ if (!value)
115
+ return "news widget config must be an object";
116
+ const providerError = providerIdError(value);
117
+ if (providerError)
118
+ return providerError;
119
+ if (typeof value.limit !== "number" ||
120
+ !Number.isInteger(value.limit) ||
121
+ value.limit < 1 ||
122
+ value.limit > 12) {
123
+ return "news widget limit must be an integer from 1 to 12";
124
+ }
125
+ const refresh = value.refreshMinutes;
126
+ return refresh !== undefined &&
127
+ (typeof refresh !== "number" ||
128
+ !Number.isInteger(refresh) ||
129
+ refresh < 1 ||
130
+ refresh > 1440)
131
+ ? "news widget refreshMinutes must be an integer from 1 to 1440"
132
+ : null;
133
+ },
134
+ weather(config) {
135
+ const value = configRecord(config);
136
+ if (!value)
137
+ return "weather widget requires a valid location";
138
+ const providerError = providerIdError(value);
139
+ if (providerError)
140
+ return providerError;
141
+ const location = value.location;
142
+ const legacy = typeof location === "string" &&
143
+ location.trim().length >= 2 &&
144
+ location.length <= 120;
145
+ const resolved = configRecord(location);
146
+ const valid = resolved !== null &&
147
+ typeof resolved.name === "string" &&
148
+ resolved.name.trim().length > 0 &&
149
+ resolved.name.length <= 120 &&
150
+ typeof resolved.lat === "number" &&
151
+ Number.isFinite(resolved.lat) &&
152
+ resolved.lat >= -90 &&
153
+ resolved.lat <= 90 &&
154
+ typeof resolved.lon === "number" &&
155
+ Number.isFinite(resolved.lon) &&
156
+ resolved.lon >= -180 &&
157
+ resolved.lon <= 180 &&
158
+ typeof resolved.timeZone === "string" &&
159
+ resolved.timeZone.trim().length > 0 &&
160
+ resolved.timeZone.length <= 120;
161
+ return legacy || valid ? null : "weather widget requires a valid location";
162
+ },
163
+ currency(config) {
164
+ const value = configRecord(config);
165
+ if (!value)
166
+ return "currency widget config is invalid";
167
+ const providerError = providerIdError(value);
168
+ if (providerError)
169
+ return providerError;
170
+ if ("currencies" in value || "home" in value) {
171
+ const currencies = value.currencies;
172
+ return typeof value.home !== "string" ||
173
+ !/^[A-Z]{3}$/.test(value.home) ||
174
+ !Array.isArray(currencies) ||
175
+ currencies.length < 1 ||
176
+ currencies.length > 3 ||
177
+ currencies.some((code) => typeof code !== "string" || !/^[A-Z]{3}$/.test(code)) ||
178
+ new Set(currencies).size !== currencies.length
179
+ ? "currency widget requires a home currency and 1 to 3 unique currencies"
180
+ : null;
181
+ }
182
+ return typeof value.base !== "string" ||
183
+ typeof value.quote !== "string" ||
184
+ !/^[A-Z]{3}$/.test(value.base) ||
185
+ !/^[A-Z]{3}$/.test(value.quote) ||
186
+ typeof value.defaultAmount !== "number" ||
187
+ !Number.isFinite(value.defaultAmount) ||
188
+ value.defaultAmount < 0
189
+ ? "currency widget requires base, quote, and defaultAmount"
190
+ : null;
191
+ },
192
+ crypto(config) {
193
+ const value = configRecord(config);
194
+ if (!value)
195
+ return "crypto widget config must be an object";
196
+ const providerError = providerIdError(value);
197
+ if (providerError)
198
+ return providerError;
199
+ const coins = value.coins;
200
+ if (!Array.isArray(coins) ||
201
+ coins.length < 1 ||
202
+ coins.length > exports.CRYPTO_MAX_COINS ||
203
+ coins.some((coin) => typeof coin !== "string" || !/^[a-z0-9-]{1,100}$/.test(coin)) ||
204
+ new Set(coins).size !== coins.length) {
205
+ return `crypto widget requires 1 to ${exports.CRYPTO_MAX_COINS} unique lowercase coin ids`;
206
+ }
207
+ return typeof value.vsCurrency !== "string" ||
208
+ !/^[A-Z]{3}$/.test(value.vsCurrency)
209
+ ? "crypto widget vsCurrency must be a 3-letter uppercase currency code"
210
+ : null;
211
+ },
212
+ apod(config) {
213
+ const value = configRecord(config);
214
+ if (!value)
215
+ return "apod widget config must be an object";
216
+ const providerError = providerIdError(value);
217
+ if (providerError)
218
+ return providerError;
219
+ return Object.keys(value).some((key) => key !== "providerId")
220
+ ? "apod widget config contains an unsupported setting"
221
+ : null;
222
+ },
223
+ };
224
+ function validateHomeWidgetConfig(type, config) {
225
+ return HOME_WIDGET_CONFIG_VALIDATORS[type](config);
226
+ }
@@ -16,13 +16,44 @@ describe("home dashboard types", () => {
16
16
  "news",
17
17
  "weather",
18
18
  "currency",
19
+ "crypto",
20
+ "apod",
19
21
  ]);
20
22
  expect((0, home_dashboard_types_1.isHomeWidgetType)("clock")).toBe(true);
21
23
  expect((0, home_dashboard_types_1.isHomeWidgetType)("analytics")).toBe(true);
22
24
  expect((0, home_dashboard_types_1.isHomeWidgetType)("utility")).toBe(true);
23
25
  expect((0, home_dashboard_types_1.isHomeWidgetType)("weather")).toBe(true);
26
+ expect((0, home_dashboard_types_1.isHomeWidgetType)("apod")).toBe(true);
24
27
  expect((0, home_dashboard_types_1.isHomeWidgetType)("stock-ticker")).toBe(false);
25
28
  });
29
+ it("validates the APOD provider selection", () => {
30
+ expect((0, home_dashboard_types_1.validateHomeWidgetConfig)("apod", {})).toBeNull();
31
+ expect((0, home_dashboard_types_1.validateHomeWidgetConfig)("apod", { providerId: "nasa-1" })).toBeNull();
32
+ expect((0, home_dashboard_types_1.validateHomeWidgetConfig)("apod", { providerId: "" })).not.toBeNull();
33
+ });
34
+ it("validates crypto widget configs", () => {
35
+ expect((0, home_dashboard_types_1.validateHomeWidgetConfig)("crypto", {
36
+ coins: ["bitcoin", "ethereum"],
37
+ vsCurrency: "BRL",
38
+ })).toBeNull();
39
+ expect((0, home_dashboard_types_1.validateHomeWidgetConfig)("crypto", {
40
+ providerId: "crypto-1",
41
+ coins: ["bitcoin"],
42
+ vsCurrency: "USD",
43
+ })).toBeNull();
44
+ const invalid = [
45
+ { coins: [], vsCurrency: "BRL" },
46
+ { coins: ["a", "b", "c", "d", "e", "f"], vsCurrency: "BRL" },
47
+ { coins: ["bitcoin", "bitcoin"], vsCurrency: "BRL" },
48
+ { coins: ["Bitcoin!"], vsCurrency: "BRL" },
49
+ { coins: ["bitcoin"], vsCurrency: "brl" },
50
+ { coins: ["bitcoin"], vsCurrency: "BRLX" },
51
+ { providerId: "", coins: ["bitcoin"], vsCurrency: "BRL" },
52
+ ];
53
+ for (const config of invalid) {
54
+ expect((0, home_dashboard_types_1.validateHomeWidgetConfig)("crypto", config)).not.toBeNull();
55
+ }
56
+ });
26
57
  it("stores a versioned allowlisted utility configuration", () => {
27
58
  const widget = {
28
59
  id: "utility-1",
@@ -133,3 +164,29 @@ describe("GeoLocation", () => {
133
164
  expect(weather.location).toBe("Cayenne");
134
165
  });
135
166
  });
167
+ describe("home widget config validation", () => {
168
+ it("uses one shared validator for current and legacy currency configs", () => {
169
+ expect((0, home_dashboard_types_1.validateHomeWidgetConfig)("currency", {
170
+ home: "BRL",
171
+ currencies: ["USD", "EUR"],
172
+ })).toBeNull();
173
+ expect((0, home_dashboard_types_1.validateHomeWidgetConfig)("currency", {
174
+ base: "USD",
175
+ quote: "EUR",
176
+ defaultAmount: 1,
177
+ })).toBeNull();
178
+ expect((0, home_dashboard_types_1.validateHomeWidgetConfig)("currency", {
179
+ home: "BRL",
180
+ currencies: ["USD", "USD"],
181
+ })).toContain("unique currencies");
182
+ });
183
+ it("applies resolved-location constraints consistently", () => {
184
+ expect((0, home_dashboard_types_1.validateHomeWidgetConfig)("weather", {
185
+ location: { name: "Recife", lat: -8.05, lon: -34.9 },
186
+ })).toBe("weather widget requires a valid location");
187
+ });
188
+ it("keeps an empty webpage placeholder but rejects a missing URL", () => {
189
+ expect((0, home_dashboard_types_1.validateHomeWidgetConfig)("webpage", { url: "" })).toBeNull();
190
+ expect((0, home_dashboard_types_1.validateHomeWidgetConfig)("webpage", {})).toBe("webpage widget config url must be a string");
191
+ });
192
+ });
@@ -1,6 +1,132 @@
1
1
  /** Shared contracts for provider-backed dashboard widgets. */
2
- export type WidgetServiceKind = "news" | "weather" | "currency";
3
- export type WidgetServiceProviderType = "rss" | "open-meteo" | "frankfurter";
2
+ export type WidgetServiceKind = "news" | "weather" | "currency" | "geo" | "crypto" | "nasa";
3
+ export type WidgetServiceSettingValue = string | number | boolean;
4
+ export type WidgetServiceSettings = Record<string, WidgetServiceSettingValue>;
5
+ export interface WidgetServiceProviderSettingsMap {
6
+ rss: {
7
+ feedUrl: string;
8
+ };
9
+ "open-meteo": Record<string, never>;
10
+ frankfurter: Record<string, never>;
11
+ "google-places": {
12
+ apiKey: string;
13
+ };
14
+ coingecko: {
15
+ apiKey?: string;
16
+ };
17
+ nasa: {
18
+ apiKey?: string;
19
+ };
20
+ }
21
+ export type WidgetServiceProviderType = keyof WidgetServiceProviderSettingsMap;
22
+ export interface WidgetServiceSettingsField {
23
+ key: string;
24
+ label: string;
25
+ input: "text" | "url" | "password";
26
+ required: boolean;
27
+ maxLength: number;
28
+ defaultValue?: string;
29
+ }
30
+ export interface WidgetServiceProviderDefinition<T extends WidgetServiceProviderType> {
31
+ type: T;
32
+ kind: WidgetServiceKind;
33
+ label: string;
34
+ settingsFields: readonly WidgetServiceSettingsField[];
35
+ secretSettingKeys: readonly (keyof WidgetServiceProviderSettingsMap[T] & string)[];
36
+ normalizeSettings(settings: WidgetServiceSettings): WidgetServiceProviderSettingsMap[T];
37
+ }
38
+ export declare const WIDGET_SERVICE_SECRET_MASK = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
39
+ declare function noSettings(): Record<string, never>;
40
+ declare function normalizeRssSettings(settings: WidgetServiceSettings): {
41
+ feedUrl: string;
42
+ };
43
+ declare function normalizeGooglePlacesSettings(settings: WidgetServiceSettings): {
44
+ apiKey: string;
45
+ };
46
+ declare function normalizeCoingeckoSettings(settings: WidgetServiceSettings): {
47
+ apiKey?: string;
48
+ };
49
+ declare function normalizeOptionalApiKey(settings: WidgetServiceSettings): {
50
+ apiKey?: string;
51
+ };
52
+ /** Single registration point for provider metadata and per-type settings schemas. */
53
+ export declare const WIDGET_SERVICE_PROVIDER_DEFINITIONS: {
54
+ rss: {
55
+ type: "rss";
56
+ kind: "news";
57
+ label: string;
58
+ settingsFields: {
59
+ key: string;
60
+ label: string;
61
+ input: "url";
62
+ required: true;
63
+ maxLength: number;
64
+ defaultValue: string;
65
+ }[];
66
+ secretSettingKeys: never[];
67
+ normalizeSettings: typeof normalizeRssSettings;
68
+ };
69
+ "open-meteo": {
70
+ type: "open-meteo";
71
+ kind: "weather";
72
+ label: string;
73
+ settingsFields: never[];
74
+ secretSettingKeys: never[];
75
+ normalizeSettings: typeof noSettings;
76
+ };
77
+ frankfurter: {
78
+ type: "frankfurter";
79
+ kind: "currency";
80
+ label: string;
81
+ settingsFields: never[];
82
+ secretSettingKeys: never[];
83
+ normalizeSettings: typeof noSettings;
84
+ };
85
+ "google-places": {
86
+ type: "google-places";
87
+ kind: "geo";
88
+ label: string;
89
+ settingsFields: {
90
+ key: string;
91
+ label: string;
92
+ input: "password";
93
+ required: true;
94
+ maxLength: number;
95
+ }[];
96
+ secretSettingKeys: "apiKey"[];
97
+ normalizeSettings: typeof normalizeGooglePlacesSettings;
98
+ };
99
+ coingecko: {
100
+ type: "coingecko";
101
+ kind: "crypto";
102
+ label: string;
103
+ settingsFields: {
104
+ key: string;
105
+ label: string;
106
+ input: "password";
107
+ required: false;
108
+ maxLength: number;
109
+ }[];
110
+ secretSettingKeys: "apiKey"[];
111
+ normalizeSettings: typeof normalizeCoingeckoSettings;
112
+ };
113
+ nasa: {
114
+ type: "nasa";
115
+ kind: "nasa";
116
+ label: string;
117
+ settingsFields: {
118
+ key: string;
119
+ label: string;
120
+ input: "password";
121
+ required: false;
122
+ maxLength: number;
123
+ }[];
124
+ secretSettingKeys: "apiKey"[];
125
+ normalizeSettings: typeof normalizeOptionalApiKey;
126
+ };
127
+ };
128
+ export declare const WIDGET_SERVICE_PROVIDER_TYPES: readonly (keyof WidgetServiceProviderSettingsMap)[];
129
+ export declare function normalizeWidgetServiceProviderSettings<T extends WidgetServiceProviderType>(type: T, settings: WidgetServiceSettings): WidgetServiceProviderSettingsMap[T];
4
130
  export type WidgetServiceResult<T> = {
5
131
  ok: true;
6
132
  value: T;
@@ -16,7 +142,13 @@ export interface WidgetServiceProviderConfig {
16
142
  name: string;
17
143
  enabled: boolean;
18
144
  active: boolean;
19
- settings: Record<string, string | number | boolean>;
145
+ settings: WidgetServiceSettings;
146
+ }
147
+ export interface WidgetServiceProviderPayload {
148
+ type: WidgetServiceProviderType;
149
+ name: string;
150
+ enabled: boolean;
151
+ settings: WidgetServiceSettings;
20
152
  }
21
153
  export interface NewsArticle {
22
154
  title: string;
@@ -53,4 +185,31 @@ export interface CurrencyRate {
53
185
  rate: number;
54
186
  date: string;
55
187
  }
188
+ export interface CryptoQuote {
189
+ /** CoinGecko-style coin id, e.g. "bitcoin". */
190
+ coinId: string;
191
+ /** Display symbol, e.g. "BTC". */
192
+ symbol: string;
193
+ /** Price in `vsCurrency`. */
194
+ price: number;
195
+ /** Uppercase ISO 4217 code the price is quoted in. */
196
+ vsCurrency: string;
197
+ change24hPercent: number;
198
+ fetchedAt: string;
199
+ }
200
+ export interface CryptoBoard {
201
+ quotes: CryptoQuote[];
202
+ fetchedAt: string;
203
+ }
204
+ export interface AstronomyPictureOfTheDay {
205
+ date: string;
206
+ title: string;
207
+ explanation: string;
208
+ mediaType: "image" | "video";
209
+ url: string;
210
+ hdUrl?: string;
211
+ thumbnailUrl?: string;
212
+ copyright?: string;
213
+ }
214
+ export {};
56
215
  //# sourceMappingURL=widget-services.types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"widget-services.types.d.ts","sourceRoot":"","sources":["../src/widget-services.types.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAC9D,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,SAAS,GAAG,UAAU,CAAC;AAChE,MAAM,MAAM,yBAAyB,GAAG,KAAK,GAAG,YAAY,GAAG,aAAa,CAAC;AAE7E,MAAM,MAAM,mBAAmB,CAAC,CAAC,IAC7B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,CAAC,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GACxC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjC,MAAM,WAAW,2BAA2B;IAC1C,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,iBAAiB,CAAC;IACxB,IAAI,EAAE,yBAAyB,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;CACrD;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd"}
1
+ {"version":3,"file":"widget-services.types.d.ts","sourceRoot":"","sources":["../src/widget-services.types.ts"],"names":[],"mappings":"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"}
@@ -1,2 +1,138 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WIDGET_SERVICE_PROVIDER_TYPES = exports.WIDGET_SERVICE_PROVIDER_DEFINITIONS = exports.WIDGET_SERVICE_SECRET_MASK = void 0;
4
+ exports.normalizeWidgetServiceProviderSettings = normalizeWidgetServiceProviderSettings;
5
+ exports.WIDGET_SERVICE_SECRET_MASK = "••••••••";
6
+ function noSettings() {
7
+ return {};
8
+ }
9
+ function normalizeRssSettings(settings) {
10
+ const feedUrl = typeof settings.feedUrl === "string" ? settings.feedUrl.trim() : "";
11
+ let parsed;
12
+ try {
13
+ parsed = new URL(feedUrl);
14
+ }
15
+ catch {
16
+ throw new Error("RSS feed URL must be a valid HTTPS URL");
17
+ }
18
+ if (parsed.protocol !== "https:")
19
+ throw new Error("RSS feed URL must use HTTPS");
20
+ const hostname = parsed.hostname.toLowerCase();
21
+ if (hostname === "localhost" ||
22
+ hostname.endsWith(".localhost") ||
23
+ hostname.endsWith(".local") ||
24
+ hostname === "::1" ||
25
+ /^127\./.test(hostname) ||
26
+ /^10\./.test(hostname) ||
27
+ /^192\.168\./.test(hostname) ||
28
+ /^169\.254\./.test(hostname) ||
29
+ /^172\.(1[6-9]|2\d|3[01])\./.test(hostname)) {
30
+ throw new Error("RSS feed URL cannot target a local or private host");
31
+ }
32
+ return { feedUrl: parsed.toString() };
33
+ }
34
+ function normalizeGooglePlacesSettings(settings) {
35
+ const apiKey = typeof settings.apiKey === "string" ? settings.apiKey.trim() : "";
36
+ if (!apiKey || apiKey === exports.WIDGET_SERVICE_SECRET_MASK) {
37
+ throw new Error("Google Places API key is required");
38
+ }
39
+ return { apiKey };
40
+ }
41
+ function normalizeCoingeckoSettings(settings) {
42
+ const apiKey = typeof settings.apiKey === "string" ? settings.apiKey.trim() : "";
43
+ // The free tier needs no key; a surviving mask means no key was ever stored.
44
+ return apiKey && apiKey !== exports.WIDGET_SERVICE_SECRET_MASK ? { apiKey } : {};
45
+ }
46
+ function normalizeOptionalApiKey(settings) {
47
+ const apiKey = typeof settings.apiKey === "string" ? settings.apiKey.trim() : "";
48
+ return apiKey && apiKey !== exports.WIDGET_SERVICE_SECRET_MASK ? { apiKey } : {};
49
+ }
50
+ /** Single registration point for provider metadata and per-type settings schemas. */
51
+ exports.WIDGET_SERVICE_PROVIDER_DEFINITIONS = {
52
+ rss: {
53
+ type: "rss",
54
+ kind: "news",
55
+ label: "RSS / Atom feed",
56
+ settingsFields: [
57
+ {
58
+ key: "feedUrl",
59
+ label: "Feed URL",
60
+ input: "url",
61
+ required: true,
62
+ maxLength: 2048,
63
+ defaultValue: "https://g1.globo.com/dynamo/rss2.xml",
64
+ },
65
+ ],
66
+ secretSettingKeys: [],
67
+ normalizeSettings: normalizeRssSettings,
68
+ },
69
+ "open-meteo": {
70
+ type: "open-meteo",
71
+ kind: "weather",
72
+ label: "Open-Meteo",
73
+ settingsFields: [],
74
+ secretSettingKeys: [],
75
+ normalizeSettings: noSettings,
76
+ },
77
+ frankfurter: {
78
+ type: "frankfurter",
79
+ kind: "currency",
80
+ label: "Frankfurter",
81
+ settingsFields: [],
82
+ secretSettingKeys: [],
83
+ normalizeSettings: noSettings,
84
+ },
85
+ "google-places": {
86
+ type: "google-places",
87
+ kind: "geo",
88
+ label: "Google Places",
89
+ settingsFields: [
90
+ {
91
+ key: "apiKey",
92
+ label: "API key",
93
+ input: "password",
94
+ required: true,
95
+ maxLength: 512,
96
+ },
97
+ ],
98
+ secretSettingKeys: ["apiKey"],
99
+ normalizeSettings: normalizeGooglePlacesSettings,
100
+ },
101
+ coingecko: {
102
+ type: "coingecko",
103
+ kind: "crypto",
104
+ label: "CoinGecko",
105
+ settingsFields: [
106
+ {
107
+ key: "apiKey",
108
+ label: "API key (optional)",
109
+ input: "password",
110
+ required: false,
111
+ maxLength: 512,
112
+ },
113
+ ],
114
+ secretSettingKeys: ["apiKey"],
115
+ normalizeSettings: normalizeCoingeckoSettings,
116
+ },
117
+ nasa: {
118
+ type: "nasa",
119
+ kind: "nasa",
120
+ label: "NASA Open APIs",
121
+ settingsFields: [
122
+ {
123
+ key: "apiKey",
124
+ label: "API key (optional; DEMO_KEY when empty)",
125
+ input: "password",
126
+ required: false,
127
+ maxLength: 512,
128
+ },
129
+ ],
130
+ secretSettingKeys: ["apiKey"],
131
+ normalizeSettings: normalizeOptionalApiKey,
132
+ },
133
+ };
134
+ exports.WIDGET_SERVICE_PROVIDER_TYPES = Object.freeze(Object.keys(exports.WIDGET_SERVICE_PROVIDER_DEFINITIONS));
135
+ function normalizeWidgetServiceProviderSettings(type, settings) {
136
+ const definition = exports.WIDGET_SERVICE_PROVIDER_DEFINITIONS[type];
137
+ return definition.normalizeSettings(settings);
138
+ }
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ const widget_services_types_1 = require("./widget-services.types");
3
4
  describe("widget service contracts", () => {
4
5
  it("uses a normalized result boundary for every provider", () => {
5
6
  const news = { ok: false, error: "offline" };
@@ -8,8 +9,63 @@ describe("widget service contracts", () => {
8
9
  value: { base: "USD", quote: "EUR", rate: 0.86, date: "2026-09-02" },
9
10
  };
10
11
  const weather = {};
12
+ const apod = {
13
+ date: "2026-09-04",
14
+ title: "The sky today",
15
+ explanation: "A test entry",
16
+ mediaType: "video",
17
+ url: "https://www.youtube.com/embed/example",
18
+ };
11
19
  expect(news.ok).toBe(false);
12
20
  expect(currency.ok).toBe(true);
13
21
  expect(weather).toEqual({});
22
+ expect(apod.mediaType).toBe("video");
23
+ });
24
+ });
25
+ describe("widget service provider definitions", () => {
26
+ it("registers every provider with its kind and settings schema", () => {
27
+ expect(widget_services_types_1.WIDGET_SERVICE_PROVIDER_TYPES).toEqual([
28
+ "rss",
29
+ "open-meteo",
30
+ "frankfurter",
31
+ "google-places",
32
+ "coingecko",
33
+ "nasa",
34
+ ]);
35
+ expect(widget_services_types_1.WIDGET_SERVICE_PROVIDER_DEFINITIONS["google-places"]).toMatchObject({
36
+ kind: "geo",
37
+ secretSettingKeys: ["apiKey"],
38
+ });
39
+ expect(widget_services_types_1.WIDGET_SERVICE_PROVIDER_DEFINITIONS.coingecko).toMatchObject({
40
+ kind: "crypto",
41
+ secretSettingKeys: ["apiKey"],
42
+ });
43
+ expect(widget_services_types_1.WIDGET_SERVICE_PROVIDER_DEFINITIONS.nasa).toMatchObject({
44
+ kind: "nasa",
45
+ secretSettingKeys: ["apiKey"],
46
+ });
47
+ });
48
+ it("keeps the CoinGecko API key optional", () => {
49
+ expect((0, widget_services_types_1.normalizeWidgetServiceProviderSettings)("coingecko", { apiKey: " k-1 " })).toEqual({ apiKey: "k-1" });
50
+ expect((0, widget_services_types_1.normalizeWidgetServiceProviderSettings)("coingecko", {})).toEqual({});
51
+ expect((0, widget_services_types_1.normalizeWidgetServiceProviderSettings)("coingecko", { apiKey: "" })).toEqual({});
52
+ });
53
+ it("keeps the NASA API key optional and trims configured keys", () => {
54
+ expect((0, widget_services_types_1.normalizeWidgetServiceProviderSettings)("nasa", {})).toEqual({});
55
+ expect((0, widget_services_types_1.normalizeWidgetServiceProviderSettings)("nasa", { apiKey: " nasa-key " })).toEqual({ apiKey: "nasa-key" });
56
+ });
57
+ it("normalizes settings through the selected provider schema", () => {
58
+ expect((0, widget_services_types_1.normalizeWidgetServiceProviderSettings)("rss", {
59
+ feedUrl: " https://example.com/feed.xml ",
60
+ })).toEqual({ feedUrl: "https://example.com/feed.xml" });
61
+ expect((0, widget_services_types_1.normalizeWidgetServiceProviderSettings)("open-meteo", { ignored: true })).toEqual({});
62
+ });
63
+ it("rejects unsafe RSS URLs and masked secrets", () => {
64
+ expect(() => (0, widget_services_types_1.normalizeWidgetServiceProviderSettings)("rss", {
65
+ feedUrl: "https://localhost/feed",
66
+ })).toThrow("local or private host");
67
+ expect(() => (0, widget_services_types_1.normalizeWidgetServiceProviderSettings)("google-places", {
68
+ apiKey: widget_services_types_1.WIDGET_SERVICE_SECRET_MASK,
69
+ })).toThrow("API key is required");
14
70
  });
15
71
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guinetik/primitives-ts",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Shared TypeScript primitives for Guinetik projects",
5
5
  "author": "Guinetik",
6
6
  "license": "MIT",