@guinetik/primitives-ts 0.14.0 → 0.14.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/dist/home-dashboard.types.d.ts +3 -1
- package/dist/home-dashboard.types.d.ts.map +1 -1
- package/dist/home-dashboard.types.js +160 -0
- package/dist/home-dashboard.types.spec.js +26 -0
- package/dist/widget-services.types.d.ts +96 -3
- package/dist/widget-services.types.d.ts.map +1 -1
- package/dist/widget-services.types.js +95 -0
- package/dist/widget-services.types.spec.js +29 -0
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { GeoLocation } from
|
|
1
|
+
import type { GeoLocation } from "./geo.types";
|
|
2
2
|
export type HomeWidgetType = "launchpad" | "clock" | "webpage" | "analytics" | "utility" | "news" | "weather" | "currency";
|
|
3
3
|
export type HomeWidgetColSpan = 1 | 2 | 3;
|
|
4
4
|
export interface LaunchpadWidgetConfig {
|
|
@@ -71,4 +71,6 @@ export declare const CLOCK_DEFAULT_TIMEZONE = "America/Sao_Paulo";
|
|
|
71
71
|
export declare const CLOCK_DEFAULT_PRESET = "aurora";
|
|
72
72
|
export declare const CLOCK_PRESETS: readonly ["aurora", "grid", "binary", "day-night", "location-photos"];
|
|
73
73
|
export declare function isHomeWidgetType(value: string): value is HomeWidgetType;
|
|
74
|
+
export type HomeWidgetConfigValidationResult = string | null;
|
|
75
|
+
export declare function validateHomeWidgetConfig(type: HomeWidgetType, config: unknown): HomeWidgetConfigValidationResult;
|
|
74
76
|
//# 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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"}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
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;
|
|
4
4
|
exports.isHomeWidgetType = isHomeWidgetType;
|
|
5
|
+
exports.validateHomeWidgetConfig = validateHomeWidgetConfig;
|
|
5
6
|
exports.UTILITY_TOOL_IDS = [
|
|
6
7
|
"unit-converter",
|
|
7
8
|
"text-workbench",
|
|
@@ -30,3 +31,162 @@ exports.CLOCK_PRESETS = [
|
|
|
30
31
|
function isHomeWidgetType(value) {
|
|
31
32
|
return exports.HOME_WIDGET_TYPES.includes(value);
|
|
32
33
|
}
|
|
34
|
+
function configRecord(config) {
|
|
35
|
+
return typeof config === "object" && config !== null && !Array.isArray(config)
|
|
36
|
+
? config
|
|
37
|
+
: null;
|
|
38
|
+
}
|
|
39
|
+
function providerIdError(config) {
|
|
40
|
+
const providerId = config.providerId;
|
|
41
|
+
return providerId !== undefined &&
|
|
42
|
+
(typeof providerId !== "string" ||
|
|
43
|
+
providerId.trim().length === 0 ||
|
|
44
|
+
providerId.length > 200)
|
|
45
|
+
? "widget providerId must be a non-empty string"
|
|
46
|
+
: null;
|
|
47
|
+
}
|
|
48
|
+
const HOME_WIDGET_CONFIG_VALIDATORS = {
|
|
49
|
+
launchpad(config) {
|
|
50
|
+
const value = configRecord(config);
|
|
51
|
+
if (!value)
|
|
52
|
+
return "launchpad widget config must be an object";
|
|
53
|
+
const ids = value.groupIds;
|
|
54
|
+
if (ids !== undefined &&
|
|
55
|
+
(!Array.isArray(ids) ||
|
|
56
|
+
ids.length === 0 ||
|
|
57
|
+
ids.some((id) => typeof id !== "string" || id.trim().length === 0) ||
|
|
58
|
+
new Set(ids).size !== ids.length)) {
|
|
59
|
+
return "launchpad widget config groupIds must be a non-empty array of unique strings";
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
},
|
|
63
|
+
clock(config) {
|
|
64
|
+
const value = configRecord(config);
|
|
65
|
+
if (!value ||
|
|
66
|
+
typeof value.timeZone !== "string" ||
|
|
67
|
+
value.timeZone.length === 0 ||
|
|
68
|
+
typeof value.preset !== "string" ||
|
|
69
|
+
value.preset.length === 0) {
|
|
70
|
+
return "clock widget config requires timeZone and preset";
|
|
71
|
+
}
|
|
72
|
+
return value.hour12 !== undefined && typeof value.hour12 !== "boolean"
|
|
73
|
+
? "clock widget hour12 must be a boolean"
|
|
74
|
+
: null;
|
|
75
|
+
},
|
|
76
|
+
webpage(config) {
|
|
77
|
+
const value = configRecord(config);
|
|
78
|
+
if (!value || typeof value.url !== "string") {
|
|
79
|
+
return "webpage widget config url must be a string";
|
|
80
|
+
}
|
|
81
|
+
return value.url.length > 0 && !/^https?:\/\//i.test(value.url)
|
|
82
|
+
? "webpage widget config url must start with http:// or https://"
|
|
83
|
+
: null;
|
|
84
|
+
},
|
|
85
|
+
analytics(config) {
|
|
86
|
+
const propertyId = configRecord(config)?.propertyId;
|
|
87
|
+
return typeof propertyId !== "string" ||
|
|
88
|
+
!/^properties\/\d+$/.test(propertyId)
|
|
89
|
+
? "analytics widget config propertyId must look like properties/123456"
|
|
90
|
+
: null;
|
|
91
|
+
},
|
|
92
|
+
utility(config) {
|
|
93
|
+
const value = configRecord(config);
|
|
94
|
+
if (!value)
|
|
95
|
+
return "utility widget config must be an object";
|
|
96
|
+
if (value.schemaVersion !== 1)
|
|
97
|
+
return "utility widget config schemaVersion must be 1";
|
|
98
|
+
if (typeof value.toolId !== "string" ||
|
|
99
|
+
!exports.UTILITY_TOOL_IDS.includes(value.toolId)) {
|
|
100
|
+
return "utility widget config contains an unsupported toolId";
|
|
101
|
+
}
|
|
102
|
+
const settings = configRecord(value.settings);
|
|
103
|
+
if (!settings)
|
|
104
|
+
return "utility widget settings must be an object";
|
|
105
|
+
return new TextEncoder().encode(JSON.stringify(settings)).length > 4096
|
|
106
|
+
? "utility widget settings are too large"
|
|
107
|
+
: null;
|
|
108
|
+
},
|
|
109
|
+
news(config) {
|
|
110
|
+
const value = configRecord(config);
|
|
111
|
+
if (!value)
|
|
112
|
+
return "news widget config must be an object";
|
|
113
|
+
const providerError = providerIdError(value);
|
|
114
|
+
if (providerError)
|
|
115
|
+
return providerError;
|
|
116
|
+
if (typeof value.limit !== "number" ||
|
|
117
|
+
!Number.isInteger(value.limit) ||
|
|
118
|
+
value.limit < 1 ||
|
|
119
|
+
value.limit > 12) {
|
|
120
|
+
return "news widget limit must be an integer from 1 to 12";
|
|
121
|
+
}
|
|
122
|
+
const refresh = value.refreshMinutes;
|
|
123
|
+
return refresh !== undefined &&
|
|
124
|
+
(typeof refresh !== "number" ||
|
|
125
|
+
!Number.isInteger(refresh) ||
|
|
126
|
+
refresh < 1 ||
|
|
127
|
+
refresh > 1440)
|
|
128
|
+
? "news widget refreshMinutes must be an integer from 1 to 1440"
|
|
129
|
+
: null;
|
|
130
|
+
},
|
|
131
|
+
weather(config) {
|
|
132
|
+
const value = configRecord(config);
|
|
133
|
+
if (!value)
|
|
134
|
+
return "weather widget requires a valid location";
|
|
135
|
+
const providerError = providerIdError(value);
|
|
136
|
+
if (providerError)
|
|
137
|
+
return providerError;
|
|
138
|
+
const location = value.location;
|
|
139
|
+
const legacy = typeof location === "string" &&
|
|
140
|
+
location.trim().length >= 2 &&
|
|
141
|
+
location.length <= 120;
|
|
142
|
+
const resolved = configRecord(location);
|
|
143
|
+
const valid = resolved !== null &&
|
|
144
|
+
typeof resolved.name === "string" &&
|
|
145
|
+
resolved.name.trim().length > 0 &&
|
|
146
|
+
resolved.name.length <= 120 &&
|
|
147
|
+
typeof resolved.lat === "number" &&
|
|
148
|
+
Number.isFinite(resolved.lat) &&
|
|
149
|
+
resolved.lat >= -90 &&
|
|
150
|
+
resolved.lat <= 90 &&
|
|
151
|
+
typeof resolved.lon === "number" &&
|
|
152
|
+
Number.isFinite(resolved.lon) &&
|
|
153
|
+
resolved.lon >= -180 &&
|
|
154
|
+
resolved.lon <= 180 &&
|
|
155
|
+
typeof resolved.timeZone === "string" &&
|
|
156
|
+
resolved.timeZone.trim().length > 0 &&
|
|
157
|
+
resolved.timeZone.length <= 120;
|
|
158
|
+
return legacy || valid ? null : "weather widget requires a valid location";
|
|
159
|
+
},
|
|
160
|
+
currency(config) {
|
|
161
|
+
const value = configRecord(config);
|
|
162
|
+
if (!value)
|
|
163
|
+
return "currency widget config is invalid";
|
|
164
|
+
const providerError = providerIdError(value);
|
|
165
|
+
if (providerError)
|
|
166
|
+
return providerError;
|
|
167
|
+
if ("currencies" in value || "home" in value) {
|
|
168
|
+
const currencies = value.currencies;
|
|
169
|
+
return typeof value.home !== "string" ||
|
|
170
|
+
!/^[A-Z]{3}$/.test(value.home) ||
|
|
171
|
+
!Array.isArray(currencies) ||
|
|
172
|
+
currencies.length < 1 ||
|
|
173
|
+
currencies.length > 3 ||
|
|
174
|
+
currencies.some((code) => typeof code !== "string" || !/^[A-Z]{3}$/.test(code)) ||
|
|
175
|
+
new Set(currencies).size !== currencies.length
|
|
176
|
+
? "currency widget requires a home currency and 1 to 3 unique currencies"
|
|
177
|
+
: null;
|
|
178
|
+
}
|
|
179
|
+
return typeof value.base !== "string" ||
|
|
180
|
+
typeof value.quote !== "string" ||
|
|
181
|
+
!/^[A-Z]{3}$/.test(value.base) ||
|
|
182
|
+
!/^[A-Z]{3}$/.test(value.quote) ||
|
|
183
|
+
typeof value.defaultAmount !== "number" ||
|
|
184
|
+
!Number.isFinite(value.defaultAmount) ||
|
|
185
|
+
value.defaultAmount < 0
|
|
186
|
+
? "currency widget requires base, quote, and defaultAmount"
|
|
187
|
+
: null;
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
function validateHomeWidgetConfig(type, config) {
|
|
191
|
+
return HOME_WIDGET_CONFIG_VALIDATORS[type](config);
|
|
192
|
+
}
|
|
@@ -133,3 +133,29 @@ describe("GeoLocation", () => {
|
|
|
133
133
|
expect(weather.location).toBe("Cayenne");
|
|
134
134
|
});
|
|
135
135
|
});
|
|
136
|
+
describe("home widget config validation", () => {
|
|
137
|
+
it("uses one shared validator for current and legacy currency configs", () => {
|
|
138
|
+
expect((0, home_dashboard_types_1.validateHomeWidgetConfig)("currency", {
|
|
139
|
+
home: "BRL",
|
|
140
|
+
currencies: ["USD", "EUR"],
|
|
141
|
+
})).toBeNull();
|
|
142
|
+
expect((0, home_dashboard_types_1.validateHomeWidgetConfig)("currency", {
|
|
143
|
+
base: "USD",
|
|
144
|
+
quote: "EUR",
|
|
145
|
+
defaultAmount: 1,
|
|
146
|
+
})).toBeNull();
|
|
147
|
+
expect((0, home_dashboard_types_1.validateHomeWidgetConfig)("currency", {
|
|
148
|
+
home: "BRL",
|
|
149
|
+
currencies: ["USD", "USD"],
|
|
150
|
+
})).toContain("unique currencies");
|
|
151
|
+
});
|
|
152
|
+
it("applies resolved-location constraints consistently", () => {
|
|
153
|
+
expect((0, home_dashboard_types_1.validateHomeWidgetConfig)("weather", {
|
|
154
|
+
location: { name: "Recife", lat: -8.05, lon: -34.9 },
|
|
155
|
+
})).toBe("weather widget requires a valid location");
|
|
156
|
+
});
|
|
157
|
+
it("keeps an empty webpage placeholder but rejects a missing URL", () => {
|
|
158
|
+
expect((0, home_dashboard_types_1.validateHomeWidgetConfig)("webpage", { url: "" })).toBeNull();
|
|
159
|
+
expect((0, home_dashboard_types_1.validateHomeWidgetConfig)("webpage", {})).toBe("webpage widget config url must be a string");
|
|
160
|
+
});
|
|
161
|
+
});
|
|
@@ -1,6 +1,92 @@
|
|
|
1
1
|
/** Shared contracts for provider-backed dashboard widgets. */
|
|
2
|
-
export type WidgetServiceKind = "news" | "weather" | "currency";
|
|
3
|
-
export type
|
|
2
|
+
export type WidgetServiceKind = "news" | "weather" | "currency" | "geo";
|
|
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
|
+
}
|
|
15
|
+
export type WidgetServiceProviderType = keyof WidgetServiceProviderSettingsMap;
|
|
16
|
+
export interface WidgetServiceSettingsField {
|
|
17
|
+
key: string;
|
|
18
|
+
label: string;
|
|
19
|
+
input: "text" | "url" | "password";
|
|
20
|
+
required: boolean;
|
|
21
|
+
maxLength: number;
|
|
22
|
+
defaultValue?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface WidgetServiceProviderDefinition<T extends WidgetServiceProviderType> {
|
|
25
|
+
type: T;
|
|
26
|
+
kind: WidgetServiceKind;
|
|
27
|
+
label: string;
|
|
28
|
+
settingsFields: readonly WidgetServiceSettingsField[];
|
|
29
|
+
secretSettingKeys: readonly (keyof WidgetServiceProviderSettingsMap[T] & string)[];
|
|
30
|
+
normalizeSettings(settings: WidgetServiceSettings): WidgetServiceProviderSettingsMap[T];
|
|
31
|
+
}
|
|
32
|
+
export declare const WIDGET_SERVICE_SECRET_MASK = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
|
|
33
|
+
declare function noSettings(): Record<string, never>;
|
|
34
|
+
declare function normalizeRssSettings(settings: WidgetServiceSettings): {
|
|
35
|
+
feedUrl: string;
|
|
36
|
+
};
|
|
37
|
+
declare function normalizeGooglePlacesSettings(settings: WidgetServiceSettings): {
|
|
38
|
+
apiKey: string;
|
|
39
|
+
};
|
|
40
|
+
/** Single registration point for provider metadata and per-type settings schemas. */
|
|
41
|
+
export declare const WIDGET_SERVICE_PROVIDER_DEFINITIONS: {
|
|
42
|
+
rss: {
|
|
43
|
+
type: "rss";
|
|
44
|
+
kind: "news";
|
|
45
|
+
label: string;
|
|
46
|
+
settingsFields: {
|
|
47
|
+
key: string;
|
|
48
|
+
label: string;
|
|
49
|
+
input: "url";
|
|
50
|
+
required: true;
|
|
51
|
+
maxLength: number;
|
|
52
|
+
defaultValue: string;
|
|
53
|
+
}[];
|
|
54
|
+
secretSettingKeys: never[];
|
|
55
|
+
normalizeSettings: typeof normalizeRssSettings;
|
|
56
|
+
};
|
|
57
|
+
"open-meteo": {
|
|
58
|
+
type: "open-meteo";
|
|
59
|
+
kind: "weather";
|
|
60
|
+
label: string;
|
|
61
|
+
settingsFields: never[];
|
|
62
|
+
secretSettingKeys: never[];
|
|
63
|
+
normalizeSettings: typeof noSettings;
|
|
64
|
+
};
|
|
65
|
+
frankfurter: {
|
|
66
|
+
type: "frankfurter";
|
|
67
|
+
kind: "currency";
|
|
68
|
+
label: string;
|
|
69
|
+
settingsFields: never[];
|
|
70
|
+
secretSettingKeys: never[];
|
|
71
|
+
normalizeSettings: typeof noSettings;
|
|
72
|
+
};
|
|
73
|
+
"google-places": {
|
|
74
|
+
type: "google-places";
|
|
75
|
+
kind: "geo";
|
|
76
|
+
label: string;
|
|
77
|
+
settingsFields: {
|
|
78
|
+
key: string;
|
|
79
|
+
label: string;
|
|
80
|
+
input: "password";
|
|
81
|
+
required: true;
|
|
82
|
+
maxLength: number;
|
|
83
|
+
}[];
|
|
84
|
+
secretSettingKeys: "apiKey"[];
|
|
85
|
+
normalizeSettings: typeof normalizeGooglePlacesSettings;
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
export declare const WIDGET_SERVICE_PROVIDER_TYPES: readonly (keyof WidgetServiceProviderSettingsMap)[];
|
|
89
|
+
export declare function normalizeWidgetServiceProviderSettings<T extends WidgetServiceProviderType>(type: T, settings: WidgetServiceSettings): WidgetServiceProviderSettingsMap[T];
|
|
4
90
|
export type WidgetServiceResult<T> = {
|
|
5
91
|
ok: true;
|
|
6
92
|
value: T;
|
|
@@ -16,7 +102,13 @@ export interface WidgetServiceProviderConfig {
|
|
|
16
102
|
name: string;
|
|
17
103
|
enabled: boolean;
|
|
18
104
|
active: boolean;
|
|
19
|
-
settings:
|
|
105
|
+
settings: WidgetServiceSettings;
|
|
106
|
+
}
|
|
107
|
+
export interface WidgetServiceProviderPayload {
|
|
108
|
+
type: WidgetServiceProviderType;
|
|
109
|
+
name: string;
|
|
110
|
+
enabled: boolean;
|
|
111
|
+
settings: WidgetServiceSettings;
|
|
20
112
|
}
|
|
21
113
|
export interface NewsArticle {
|
|
22
114
|
title: string;
|
|
@@ -53,4 +145,5 @@ export interface CurrencyRate {
|
|
|
53
145
|
rate: number;
|
|
54
146
|
date: string;
|
|
55
147
|
}
|
|
148
|
+
export {};
|
|
56
149
|
//# 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;
|
|
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,97 @@
|
|
|
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
|
+
/** Single registration point for provider metadata and per-type settings schemas. */
|
|
42
|
+
exports.WIDGET_SERVICE_PROVIDER_DEFINITIONS = {
|
|
43
|
+
rss: {
|
|
44
|
+
type: "rss",
|
|
45
|
+
kind: "news",
|
|
46
|
+
label: "RSS / Atom feed",
|
|
47
|
+
settingsFields: [
|
|
48
|
+
{
|
|
49
|
+
key: "feedUrl",
|
|
50
|
+
label: "Feed URL",
|
|
51
|
+
input: "url",
|
|
52
|
+
required: true,
|
|
53
|
+
maxLength: 2048,
|
|
54
|
+
defaultValue: "https://g1.globo.com/dynamo/rss2.xml",
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
secretSettingKeys: [],
|
|
58
|
+
normalizeSettings: normalizeRssSettings,
|
|
59
|
+
},
|
|
60
|
+
"open-meteo": {
|
|
61
|
+
type: "open-meteo",
|
|
62
|
+
kind: "weather",
|
|
63
|
+
label: "Open-Meteo",
|
|
64
|
+
settingsFields: [],
|
|
65
|
+
secretSettingKeys: [],
|
|
66
|
+
normalizeSettings: noSettings,
|
|
67
|
+
},
|
|
68
|
+
frankfurter: {
|
|
69
|
+
type: "frankfurter",
|
|
70
|
+
kind: "currency",
|
|
71
|
+
label: "Frankfurter",
|
|
72
|
+
settingsFields: [],
|
|
73
|
+
secretSettingKeys: [],
|
|
74
|
+
normalizeSettings: noSettings,
|
|
75
|
+
},
|
|
76
|
+
"google-places": {
|
|
77
|
+
type: "google-places",
|
|
78
|
+
kind: "geo",
|
|
79
|
+
label: "Google Places",
|
|
80
|
+
settingsFields: [
|
|
81
|
+
{
|
|
82
|
+
key: "apiKey",
|
|
83
|
+
label: "API key",
|
|
84
|
+
input: "password",
|
|
85
|
+
required: true,
|
|
86
|
+
maxLength: 512,
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
secretSettingKeys: ["apiKey"],
|
|
90
|
+
normalizeSettings: normalizeGooglePlacesSettings,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
exports.WIDGET_SERVICE_PROVIDER_TYPES = Object.freeze(Object.keys(exports.WIDGET_SERVICE_PROVIDER_DEFINITIONS));
|
|
94
|
+
function normalizeWidgetServiceProviderSettings(type, settings) {
|
|
95
|
+
const definition = exports.WIDGET_SERVICE_PROVIDER_DEFINITIONS[type];
|
|
96
|
+
return definition.normalizeSettings(settings);
|
|
97
|
+
}
|
|
@@ -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" };
|
|
@@ -13,3 +14,31 @@ describe("widget service contracts", () => {
|
|
|
13
14
|
expect(weather).toEqual({});
|
|
14
15
|
});
|
|
15
16
|
});
|
|
17
|
+
describe("widget service provider definitions", () => {
|
|
18
|
+
it("registers every provider with its kind and settings schema", () => {
|
|
19
|
+
expect(widget_services_types_1.WIDGET_SERVICE_PROVIDER_TYPES).toEqual([
|
|
20
|
+
"rss",
|
|
21
|
+
"open-meteo",
|
|
22
|
+
"frankfurter",
|
|
23
|
+
"google-places",
|
|
24
|
+
]);
|
|
25
|
+
expect(widget_services_types_1.WIDGET_SERVICE_PROVIDER_DEFINITIONS["google-places"]).toMatchObject({
|
|
26
|
+
kind: "geo",
|
|
27
|
+
secretSettingKeys: ["apiKey"],
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
it("normalizes settings through the selected provider schema", () => {
|
|
31
|
+
expect((0, widget_services_types_1.normalizeWidgetServiceProviderSettings)("rss", {
|
|
32
|
+
feedUrl: " https://example.com/feed.xml ",
|
|
33
|
+
})).toEqual({ feedUrl: "https://example.com/feed.xml" });
|
|
34
|
+
expect((0, widget_services_types_1.normalizeWidgetServiceProviderSettings)("open-meteo", { ignored: true })).toEqual({});
|
|
35
|
+
});
|
|
36
|
+
it("rejects unsafe RSS URLs and masked secrets", () => {
|
|
37
|
+
expect(() => (0, widget_services_types_1.normalizeWidgetServiceProviderSettings)("rss", {
|
|
38
|
+
feedUrl: "https://localhost/feed",
|
|
39
|
+
})).toThrow("local or private host");
|
|
40
|
+
expect(() => (0, widget_services_types_1.normalizeWidgetServiceProviderSettings)("google-places", {
|
|
41
|
+
apiKey: widget_services_types_1.WIDGET_SERVICE_SECRET_MASK,
|
|
42
|
+
})).toThrow("API key is required");
|
|
43
|
+
});
|
|
44
|
+
});
|