@flowselections/floriday-authenticatie-module 1.0.16 → 1.0.18

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.
Files changed (64) hide show
  1. package/dist-lib/components/floriday/FlorydayConnectionPage.d.ts.map +1 -1
  2. package/dist-lib/components/floriday/FlorydayConnectionPage.js +2 -1
  3. package/dist-lib/components/settings/FloricodeSettingsCard.d.ts +2 -0
  4. package/dist-lib/components/settings/FloricodeSettingsCard.d.ts.map +1 -0
  5. package/dist-lib/components/settings/FloricodeSettingsCard.js +235 -0
  6. package/dist-lib/components/settings/FlorydaySettingsCard.js +1 -1
  7. package/dist-lib/index.d.ts.map +1 -1
  8. package/dist-lib/index.js +2 -0
  9. package/dist-lib/integrations/supabase/auth-middleware.d.ts +484 -3
  10. package/dist-lib/integrations/supabase/auth-middleware.d.ts.map +1 -1
  11. package/dist-lib/integrations/supabase/client.d.ts +484 -3
  12. package/dist-lib/integrations/supabase/client.d.ts.map +1 -1
  13. package/dist-lib/integrations/supabase/client.server.d.ts +484 -3
  14. package/dist-lib/integrations/supabase/client.server.d.ts.map +1 -1
  15. package/dist-lib/integrations/supabase/types.d.ts +492 -3
  16. package/dist-lib/integrations/supabase/types.d.ts.map +1 -1
  17. package/dist-lib/lib/floricode-credentials.server.d.ts +21 -0
  18. package/dist-lib/lib/floricode-credentials.server.d.ts.map +1 -0
  19. package/dist-lib/lib/floricode-credentials.server.js +158 -0
  20. package/dist-lib/lib/floricode-product-features.d.ts.map +1 -1
  21. package/dist-lib/lib/floricode-product-features.js +19 -33
  22. package/dist-lib/lib/floricode-regulatory-features.d.ts.map +1 -1
  23. package/dist-lib/lib/floricode-regulatory-features.js +51 -41
  24. package/dist-lib/lib/floricode-sync.server.d.ts +35 -0
  25. package/dist-lib/lib/floricode-sync.server.d.ts.map +1 -0
  26. package/dist-lib/lib/floricode-sync.server.js +211 -0
  27. package/dist-lib/lib/floricode.functions.d.ts +78555 -0
  28. package/dist-lib/lib/floricode.functions.d.ts.map +1 -0
  29. package/dist-lib/lib/floricode.functions.js +178 -0
  30. package/dist-lib/lib/floriday-auction-letters.functions.d.ts +484 -3
  31. package/dist-lib/lib/floriday-auction-letters.functions.d.ts.map +1 -1
  32. package/dist-lib/lib/floriday-auto-process.server.d.ts +34 -0
  33. package/dist-lib/lib/floriday-auto-process.server.d.ts.map +1 -0
  34. package/dist-lib/lib/floriday-auto-process.server.js +365 -0
  35. package/dist-lib/lib/floriday-fulfillment-lookup.server.d.ts +19 -0
  36. package/dist-lib/lib/floriday-fulfillment-lookup.server.d.ts.map +1 -0
  37. package/dist-lib/lib/floriday-fulfillment-lookup.server.js +130 -0
  38. package/dist-lib/lib/floriday-gateway-auth.d.ts +24 -0
  39. package/dist-lib/lib/floriday-gateway-auth.d.ts.map +1 -0
  40. package/dist-lib/lib/floriday-gateway-auth.js +127 -0
  41. package/dist-lib/lib/floriday-labels.functions.d.ts +484 -3
  42. package/dist-lib/lib/floriday-labels.functions.d.ts.map +1 -1
  43. package/dist-lib/lib/floriday-labels.functions.js +3 -2
  44. package/dist-lib/lib/floriday-public.functions.d.ts +2904 -18
  45. package/dist-lib/lib/floriday-public.functions.d.ts.map +1 -1
  46. package/dist-lib/lib/floriday-rate-limit.server.d.ts +21 -0
  47. package/dist-lib/lib/floriday-rate-limit.server.d.ts.map +1 -0
  48. package/dist-lib/lib/floriday-rate-limit.server.js +75 -0
  49. package/dist-lib/lib/floriday-stickers.functions.d.ts +484 -3
  50. package/dist-lib/lib/floriday-stickers.functions.d.ts.map +1 -1
  51. package/dist-lib/lib/floriday-sync.functions.d.ts +15972 -99
  52. package/dist-lib/lib/floriday-sync.functions.d.ts.map +1 -1
  53. package/dist-lib/lib/floriday-writes.functions.d.ts +2952 -42
  54. package/dist-lib/lib/floriday-writes.functions.d.ts.map +1 -1
  55. package/dist-lib/lib/floriday-writes.functions.js +29 -4
  56. package/dist-lib/lib/floriday.functions.d.ts +9054 -198
  57. package/dist-lib/lib/floriday.functions.d.ts.map +1 -1
  58. package/dist-lib/lib/floriday.functions.js +39 -4
  59. package/dist-lib/lib/planning-helpers.d.ts +30 -0
  60. package/dist-lib/lib/planning-helpers.d.ts.map +1 -0
  61. package/dist-lib/lib/planning-helpers.js +62 -0
  62. package/package.json +5 -3
  63. package/public/flowselections-assets/floriday-authenticatie/floricode-logo.png +0 -0
  64. package/public/flowselections-assets/floriday-authenticatie/floriday-logo.png +0 -0
@@ -0,0 +1,21 @@
1
+ interface Credentials {
2
+ clientId: string;
3
+ clientSecret: string;
4
+ source: "database" | "env";
5
+ }
6
+ export declare function encryptClientSecret(plaintext: string): Promise<string>;
7
+ export declare function decryptClientSecret(ciphertext: string): Promise<string>;
8
+ export declare function getFloricodeCredentials(): Promise<Credentials>;
9
+ export declare function getFloricodeAccessToken(scope: string): Promise<string>;
10
+ /**
11
+ * Test a single scope to validate credentials work. Writes the result back to
12
+ * `floricode_connection_settings.last_token_test_*`.
13
+ */
14
+ export declare function testFloricodeCredentials(): Promise<{
15
+ ok: boolean;
16
+ error?: string;
17
+ scope: string;
18
+ }>;
19
+ export declare function invalidateFloricodeTokenCache(): void;
20
+ export {};
21
+ //# sourceMappingURL=floricode-credentials.server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"floricode-credentials.server.d.ts","sourceRoot":"","sources":["../../src/lib/floricode-credentials.server.ts"],"names":[],"mappings":"AAgBA,UAAU,WAAW;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,UAAU,GAAG,KAAK,CAAC;CAC5B;AA0CD,wBAAsB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAc5E;AAED,wBAAsB,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAgB7E;AAiBD,wBAAsB,uBAAuB,IAAI,OAAO,CAAC,WAAW,CAAC,CAWpE;AAyBD,wBAAsB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAS5E;AAED;;;GAGG;AACH,wBAAsB,wBAAwB,IAAI,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAoBxG;AAED,wBAAgB,6BAA6B,IAAI,IAAI,CAEpD"}
@@ -0,0 +1,158 @@
1
+ // ============================================================================
2
+ // Floricode credentials store (server-only)
3
+ // ----------------------------------------------------------------------------
4
+ // Centrale plek voor Floricode OAuth-inloggegevens. Waarden komen uit de tabel
5
+ // `public.floricode_connection_settings` en worden versleuteld opgeslagen met
6
+ // AES-256-GCM, met de encryption key uit env-var `FLORICODE_SECRET_ENCRYPTION_KEY`.
7
+ // Fallback naar env-vars `FLORICODE_CLIENT_ID/SECRET` bestaat alleen voor
8
+ // migratieperiode zolang de UI nog niet ingevuld is.
9
+ // ============================================================================
10
+ // Web Crypto API — werkt in browser, Node 15+ en Deno. Geen Node "crypto" import,
11
+ // zodat dist-lib client-side gebundeld kan worden zonder Node builtins te resolven.
12
+ import { supabaseAdmin } from "../integrations/supabase/client.server";
13
+ const FLORICODE_TOKEN_URL = "https://api.floricode.com/oauth/token";
14
+ const tokenCache = new Map();
15
+ function getSubtle() {
16
+ const subtle = globalThis.crypto?.subtle;
17
+ if (!subtle)
18
+ throw new Error("Web Crypto (crypto.subtle) is niet beschikbaar in deze runtime.");
19
+ return subtle;
20
+ }
21
+ function toBase64(bytes) {
22
+ let bin = "";
23
+ for (let i = 0; i < bytes.length; i++)
24
+ bin += String.fromCharCode(bytes[i]);
25
+ // btoa is beschikbaar in browser, Node 16+ en Deno.
26
+ return globalThis.btoa(bin);
27
+ }
28
+ function fromBase64(b64) {
29
+ const bin = globalThis.atob(b64);
30
+ const out = new Uint8Array(bin.length);
31
+ for (let i = 0; i < bin.length; i++)
32
+ out[i] = bin.charCodeAt(i);
33
+ return out;
34
+ }
35
+ async function getEncryptionKey() {
36
+ const raw = process.env.FLORICODE_SECRET_ENCRYPTION_KEY;
37
+ if (!raw) {
38
+ throw new Error("FLORICODE_SECRET_ENCRYPTION_KEY ontbreekt — kan Floricode-credentials niet ontsleutelen.");
39
+ }
40
+ const subtle = getSubtle();
41
+ // Normaliseer naar 32 bytes via SHA-256, ongeacht lengte/format van de bron.
42
+ const digest = await subtle.digest("SHA-256", new TextEncoder().encode(raw));
43
+ return subtle.importKey("raw", digest, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
44
+ }
45
+ export async function encryptClientSecret(plaintext) {
46
+ const subtle = getSubtle();
47
+ const key = await getEncryptionKey();
48
+ const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
49
+ const cipherBuf = await subtle.encrypt({ name: "AES-GCM", iv: iv }, key, new TextEncoder().encode(plaintext));
50
+ const cipherBytes = new Uint8Array(cipherBuf);
51
+ // AES-GCM in WebCrypto plakt de 16-byte auth tag aan het einde van de ciphertext.
52
+ const tag = cipherBytes.slice(cipherBytes.length - 16);
53
+ const enc = cipherBytes.slice(0, cipherBytes.length - 16);
54
+ return `v1.${toBase64(iv)}.${toBase64(tag)}.${toBase64(enc)}`;
55
+ }
56
+ export async function decryptClientSecret(ciphertext) {
57
+ if (!ciphertext.startsWith("v1.")) {
58
+ throw new Error("Onbekend ciphertext-formaat voor Floricode-credential.");
59
+ }
60
+ const [, ivB64, tagB64, dataB64] = ciphertext.split(".");
61
+ if (!ivB64 || !tagB64 || !dataB64)
62
+ throw new Error("Ongeldig ciphertext-formaat.");
63
+ const subtle = getSubtle();
64
+ const key = await getEncryptionKey();
65
+ const iv = fromBase64(ivB64);
66
+ const tag = fromBase64(tagB64);
67
+ const data = fromBase64(dataB64);
68
+ const combined = new Uint8Array(data.length + tag.length);
69
+ combined.set(data, 0);
70
+ combined.set(tag, data.length);
71
+ const plainBuf = await subtle.decrypt({ name: "AES-GCM", iv: iv }, key, combined);
72
+ return new TextDecoder().decode(plainBuf);
73
+ }
74
+ async function loadCredentialsFromDb() {
75
+ const { data, error } = await supabaseAdmin
76
+ .from("floricode_connection_settings")
77
+ .select("client_id, client_secret_ciphertext")
78
+ .eq("id", 1)
79
+ .maybeSingle();
80
+ if (error)
81
+ throw new Error(`Kon Floricode-instellingen niet laden: ${error.message}`);
82
+ if (!data?.client_id || !data?.client_secret_ciphertext)
83
+ return null;
84
+ return {
85
+ clientId: data.client_id,
86
+ clientSecret: await decryptClientSecret(data.client_secret_ciphertext),
87
+ source: "database",
88
+ };
89
+ }
90
+ export async function getFloricodeCredentials() {
91
+ const fromDb = await loadCredentialsFromDb();
92
+ if (fromDb)
93
+ return fromDb;
94
+ const clientId = process.env.FLORICODE_CLIENT_ID;
95
+ const clientSecret = process.env.FLORICODE_CLIENT_SECRET;
96
+ if (clientId && clientSecret) {
97
+ return { clientId, clientSecret, source: "env" };
98
+ }
99
+ throw new Error("Floricode-credentials ontbreken. Vul ze in via de Floricode-instellingen in het beheerscherm.");
100
+ }
101
+ async function requestToken(scope, creds) {
102
+ const body = new URLSearchParams({
103
+ grant_type: "client_credentials",
104
+ client_id: creds.clientId,
105
+ client_secret: creds.clientSecret,
106
+ scope,
107
+ });
108
+ const res = await fetch(FLORICODE_TOKEN_URL, {
109
+ method: "POST",
110
+ headers: { "content-type": "application/x-www-form-urlencoded" },
111
+ body,
112
+ });
113
+ const json = (await res.json().catch(() => ({})));
114
+ if (!res.ok || !json?.access_token) {
115
+ throw new Error(`Floricode token (${scope}) mislukt (${res.status}): ${json?.error_description ?? json?.error ?? res.statusText}`);
116
+ }
117
+ return { token: String(json.access_token), expiresIn: Number(json.expires_in ?? 3600) };
118
+ }
119
+ export async function getFloricodeAccessToken(scope) {
120
+ const cached = tokenCache.get(scope);
121
+ const now = Date.now();
122
+ // 60s safety margin
123
+ if (cached && cached.expiresAt - 60000 > now)
124
+ return cached.token;
125
+ const creds = await getFloricodeCredentials();
126
+ const { token, expiresIn } = await requestToken(scope, creds);
127
+ tokenCache.set(scope, { token, expiresAt: now + expiresIn * 1000 });
128
+ return token;
129
+ }
130
+ /**
131
+ * Test a single scope to validate credentials work. Writes the result back to
132
+ * `floricode_connection_settings.last_token_test_*`.
133
+ */
134
+ export async function testFloricodeCredentials() {
135
+ const scope = "vbn_productfeature:read";
136
+ let ok = false;
137
+ let error;
138
+ try {
139
+ const creds = await getFloricodeCredentials();
140
+ await requestToken(scope, creds);
141
+ ok = true;
142
+ }
143
+ catch (e) {
144
+ error = String(e?.message ?? e);
145
+ }
146
+ await supabaseAdmin
147
+ .from("floricode_connection_settings")
148
+ .update({
149
+ last_token_test_at: new Date().toISOString(),
150
+ last_token_test_ok: ok,
151
+ last_token_test_error: ok ? null : error ?? "onbekende fout",
152
+ })
153
+ .eq("id", 1);
154
+ return { ok, error, scope };
155
+ }
156
+ export function invalidateFloricodeTokenCache() {
157
+ tokenCache.clear();
158
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"floricode-product-features.d.ts","sourceRoot":"","sources":["../../src/lib/floricode-product-features.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAShE,QAAA,MAAM,yBAAyB,+BAA+B,CAAC;AAc/D,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,QAAQ,EAAE,OAAO,CAAC;IAClB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,aAAa,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;IAC7D,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAuRD,wBAAsB,mCAAmC,CACvD,QAAQ,EAAE,cAAc,EACxB,WAAW,EAAE,mBAAmB,EAChC,gBAAgB,EAAE,MAAM,GAAG,MAAM,EACjC,OAAO,GAAE;IAAE,YAAY,CAAC,EAAE,OAAO,CAAA;CAAO,GACvC,OAAO,CAAC;IACT,EAAE,EAAE,IAAI,CAAC;IACT,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,EAAE,OAAO,GAAG,gBAAgB,CAAC;IACnC,YAAY,EAAE,OAAO,yBAAyB,CAAC;IAC/C,eAAe,EAAE,qBAAqB,EAAE,CAAC;CAC1C,CAAC,CAiFD;AAED,wBAAsB,4BAA4B,CAChD,QAAQ,EAAE,cAAc,EACxB,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAO;;;;;;;;GAiF1D"}
1
+ {"version":3,"file":"floricode-product-features.d.ts","sourceRoot":"","sources":["../../src/lib/floricode-product-features.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAShE,QAAA,MAAM,yBAAyB,+BAA+B,CAAC;AAc/D,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,QAAQ,EAAE,OAAO,CAAC;IAClB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,aAAa,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;IAC7D,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAoQD,wBAAsB,mCAAmC,CACvD,QAAQ,EAAE,cAAc,EACxB,WAAW,EAAE,mBAAmB,EAChC,gBAAgB,EAAE,MAAM,GAAG,MAAM,EACjC,OAAO,GAAE;IAAE,YAAY,CAAC,EAAE,OAAO,CAAA;CAAO,GACvC,OAAO,CAAC;IACT,EAAE,EAAE,IAAI,CAAC;IACT,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,EAAE,OAAO,GAAG,gBAAgB,CAAC;IACnC,YAAY,EAAE,OAAO,yBAAyB,CAAC;IAC/C,eAAe,EAAE,qBAAqB,EAAE,CAAC;CAC1C,CAAC,CAiFD;AAED,wBAAsB,4BAA4B,CAChD,QAAQ,EAAE,cAAc,EACxB,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAO;;;;;;;;GAiF1D"}
@@ -1,5 +1,5 @@
1
1
  import { fetchFloricodeRegulatoryFeaturesForProduct, upsertRegulatoryFeatureRecords, } from "./floricode-regulatory-features";
2
- const FLORICODE_TOKEN_URL = "https://api.floricode.com/oauth/token";
2
+ import { getFloricodeAccessToken as getSharedFloricodeToken } from "./floricode-credentials.server";
3
3
  const FLORICODE_PRODUCT_FEATURE_URL = "https://api.floricode.com/v2/VBN/ProductFeature";
4
4
  const PRODUCT_FEATURE_SCOPE = "vbn_productfeature:read";
5
5
  const PRODUCT_FEATURE_LIST_NAME = "vbn-product-characteristic";
@@ -20,29 +20,7 @@ function chunk(items, size) {
20
20
  return out;
21
21
  }
22
22
  async function getFloricodeAccessToken() {
23
- const clientId = process.env.FLORICODE_CLIENT_ID;
24
- const clientSecret = process.env.FLORICODE_CLIENT_SECRET;
25
- if (!clientId || !clientSecret) {
26
- throw new Error("Floricode credentials ontbreken (FLORICODE_CLIENT_ID/FLORICODE_CLIENT_SECRET)");
27
- }
28
- const body = new URLSearchParams({
29
- grant_type: "client_credentials",
30
- client_id: clientId,
31
- client_secret: clientSecret,
32
- scope: PRODUCT_FEATURE_SCOPE,
33
- });
34
- const res = await fetch(FLORICODE_TOKEN_URL, {
35
- method: "POST",
36
- headers: { "content-type": "application/x-www-form-urlencoded" },
37
- body,
38
- });
39
- const json = (await res.json().catch(() => ({})));
40
- if (!res.ok) {
41
- throw new Error(`Floricode token-aanvraag mislukt (${res.status}): ${json?.error_description ?? json?.error ?? res.statusText}`);
42
- }
43
- if (!json?.access_token)
44
- throw new Error("Floricode token-response bevat geen access_token");
45
- return String(json.access_token);
23
+ return getSharedFloricodeToken(PRODUCT_FEATURE_SCOPE);
46
24
  }
47
25
  async function fetchFloricodeProductFeaturesForProduct(productCode) {
48
26
  const token = await getFloricodeAccessToken();
@@ -74,21 +52,29 @@ async function fetchFloricodeProductFeaturesPage(token, skip, top) {
74
52
  return Array.isArray(json?.value) ? json.value : [];
75
53
  }
76
54
  async function upsertFloricodeProductFeatureRecords(supabase, items) {
77
- const rows = items
78
- .filter((item) => item?.product_id != null && item?.feature_type_id)
79
- .map((item) => {
55
+ const dedup = new Map();
56
+ const now = new Date().toISOString();
57
+ for (const item of items) {
58
+ if (item?.product_id == null || !item?.feature_type_id)
59
+ continue;
80
60
  const productCode = String(item.product_id);
81
61
  const featureCode = String(item.feature_type_id);
82
62
  const valueCode = item.feature_value_code == null ? "" : String(item.feature_value_code);
83
- return {
63
+ const record_id = `${productCode}:${featureCode}:${valueCode}`;
64
+ // Later occurrences win: keep the newest change_date_time
65
+ const existing = dedup.get(record_id);
66
+ if (existing && existing.change_date_time && item.change_date_time && existing.change_date_time > item.change_date_time)
67
+ continue;
68
+ dedup.set(record_id, {
84
69
  resource: PRODUCT_FEATURE_RESOURCE,
85
- record_id: `${productCode}:${featureCode}:${valueCode}`,
70
+ record_id,
86
71
  data: item,
87
72
  change_date_time: item.change_date_time ?? null,
88
- last_synced_at: new Date().toISOString(),
89
- };
90
- });
91
- for (const part of chunk(rows, 1000)) {
73
+ last_synced_at: now,
74
+ });
75
+ }
76
+ const rows = Array.from(dedup.values());
77
+ for (const part of chunk(rows, 250)) {
92
78
  const { error } = await supabase
93
79
  .from("floricode_records")
94
80
  .upsert(part, { onConflict: "resource,record_id" });
@@ -1 +1 @@
1
- {"version":3,"file":"floricode-regulatory-features.d.ts","sourceRoot":"","sources":["../../src/lib/floricode-regulatory-features.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAY5D,UAAU,cAAc;IACtB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,eAAe,EAAE,MAAM,CAAC;IACxB,kBAAkB,EAAE,MAAM,GAAG,MAAM,CAAC;IACpC,kBAAkB,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC5C,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED,UAAU,cAAc;IACtB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAoCD,wBAAsB,0CAA0C,CAC9D,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,cAAc,EAAE,CAAC,CAgB3B;AAkCD,wBAAsB,8BAA8B,CAClD,QAAQ,EAAE,cAAc,EACxB,KAAK,EAAE,cAAc,EAAE,GACtB,OAAO,CAAC,MAAM,CAAC,CAqBjB;AAED,wBAAsB,2BAA2B,CAC/C,QAAQ,EAAE,cAAc,EACxB,KAAK,EAAE,cAAc,EAAE,GACtB,OAAO,CAAC,MAAM,CAAC,CAqBjB;AAiBD,wBAAsB,4BAA4B,CAAC,QAAQ,EAAE,cAAc;;;;;;GA2B1E;AAED,wBAAsB,+BAA+B,CACnD,QAAQ,EAAE,cAAc,EACxB,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO;;;;;;;;;;;;;;;;;;;;;;GAwI5E"}
1
+ {"version":3,"file":"floricode-regulatory-features.d.ts","sourceRoot":"","sources":["../../src/lib/floricode-regulatory-features.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAY5D,UAAU,cAAc;IACtB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,eAAe,EAAE,MAAM,CAAC;IACxB,kBAAkB,EAAE,MAAM,GAAG,MAAM,CAAC;IACpC,kBAAkB,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC5C,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAED,UAAU,cAAc;IACtB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAYD,wBAAsB,0CAA0C,CAC9D,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,cAAc,EAAE,CAAC,CAgB3B;AAiDD,wBAAsB,8BAA8B,CAClD,QAAQ,EAAE,cAAc,EACxB,KAAK,EAAE,cAAc,EAAE,GACtB,OAAO,CAAC,MAAM,CAAC,CAqBjB;AAED,wBAAsB,2BAA2B,CAC/C,QAAQ,EAAE,cAAc,EACxB,KAAK,EAAE,cAAc,EAAE,GACtB,OAAO,CAAC,MAAM,CAAC,CAqBjB;AAiBD,wBAAsB,4BAA4B,CAAC,QAAQ,EAAE,cAAc;;;;;;GA2B1E;AAED,wBAAsB,+BAA+B,CACnD,QAAQ,EAAE,cAAc,EACxB,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO;;;;;;;;;;;;;;;;;;;;;;GAoK5E"}
@@ -1,4 +1,4 @@
1
- const FLORICODE_TOKEN_URL = "https://api.floricode.com/oauth/token";
1
+ import { getFloricodeAccessToken } from "./floricode-credentials.server";
2
2
  const REG_FEATURE_TYPE_URL = "https://api.floricode.com/v2/VBN/RegulatoryFeatureType";
3
3
  const REGULATION_TYPE_URL = "https://api.floricode.com/v2/VBN/RegulationType";
4
4
  const REG_FEATURE_TYPE_RESOURCE = "RegulatoryFeatureType";
@@ -12,27 +12,7 @@ function chunk(items, size) {
12
12
  return out;
13
13
  }
14
14
  async function getFloricodeToken(scope) {
15
- const clientId = process.env.FLORICODE_CLIENT_ID;
16
- const clientSecret = process.env.FLORICODE_CLIENT_SECRET;
17
- if (!clientId || !clientSecret) {
18
- throw new Error("Floricode credentials ontbreken (FLORICODE_CLIENT_ID/FLORICODE_CLIENT_SECRET)");
19
- }
20
- const body = new URLSearchParams({
21
- grant_type: "client_credentials",
22
- client_id: clientId,
23
- client_secret: clientSecret,
24
- scope,
25
- });
26
- const res = await fetch(FLORICODE_TOKEN_URL, {
27
- method: "POST",
28
- headers: { "content-type": "application/x-www-form-urlencoded" },
29
- body,
30
- });
31
- const json = (await res.json().catch(() => ({})));
32
- if (!res.ok || !json?.access_token) {
33
- throw new Error(`Floricode token (${scope}) mislukt (${res.status}): ${json?.error_description ?? json?.error ?? res.statusText}`);
34
- }
35
- return String(json.access_token);
15
+ return getFloricodeAccessToken(scope);
36
16
  }
37
17
  export async function fetchFloricodeRegulatoryFeaturesForProduct(productCode) {
38
18
  const token = await getFloricodeToken(REG_FEATURE_TYPE_SCOPE);
@@ -47,11 +27,18 @@ export async function fetchFloricodeRegulatoryFeaturesForProduct(productCode) {
47
27
  }
48
28
  return Array.isArray(json?.value) ? json.value : [];
49
29
  }
50
- async function fetchRegFeatureTypePage(token, skip, top) {
30
+ async function fetchRegFeatureTypePage(token, skip, top, sinceIso) {
51
31
  const url = new URL(REG_FEATURE_TYPE_URL);
52
32
  url.searchParams.set("$top", String(top));
53
33
  url.searchParams.set("$skip", String(skip));
54
- url.searchParams.set("$orderby", "product_id,feature_type_id");
34
+ // Strict-unique tuple so OData $skip pagination is deterministic.
35
+ // Without regulation_type_id in the sort key, rows with multiple
36
+ // regulation_type_ids per (product, feature) can be silently skipped
37
+ // between pages — that's how S96/S97/S98/S99 fell out of the cache.
38
+ url.searchParams.set("$orderby", ["change_date_time", "product_id", "feature_type_id", "regulation_type_id"].join(","));
39
+ if (sinceIso) {
40
+ url.searchParams.set("$filter", `change_date_time gt ${sinceIso}`);
41
+ }
55
42
  const res = await fetch(url.toString(), { headers: { authorization: `Bearer ${token}` } });
56
43
  const json = (await res.json().catch(() => ({})));
57
44
  if (!res.ok) {
@@ -84,7 +71,7 @@ export async function upsertRegulatoryFeatureRecords(supabase, items) {
84
71
  });
85
72
  }
86
73
  const rows = Array.from(dedup.values());
87
- for (const part of chunk(rows, 1000)) {
74
+ for (const part of chunk(rows, 250)) {
88
75
  const { error } = await supabase
89
76
  .from("floricode_records")
90
77
  .upsert(part, { onConflict: "resource,record_id" });
@@ -136,7 +123,7 @@ export async function syncFloricodeRegulationTypes(supabase) {
136
123
  const items = await fetchRegulationTypeAll(token);
137
124
  const upserted = await upsertRegulationTypeRecords(supabase, items);
138
125
  await setSyncState(supabase, REGULATION_TYPE_RESOURCE, {
139
- status: "success",
126
+ status: "ok",
140
127
  error_message: null,
141
128
  last_sequence_number: upserted,
142
129
  last_sync_at: new Date().toISOString(),
@@ -161,18 +148,31 @@ export async function syncFloricodeRegulatoryFeatures(supabase, options = {}) {
161
148
  // Read cursor from state so we can resume where the previous invocation stopped.
162
149
  let skip = 0;
163
150
  let maxChangeDateTime = null;
151
+ // sinceIso: watermark applied via $filter on subsequent runs after a full scan.
152
+ // - status 'running': we're mid full-scan or mid incremental-scan; keep the
153
+ // cursor/watermark that pass started with (do not filter differently mid-run).
154
+ // - status 'ok' with a watermark: kick off an incremental scan of only newer rows.
155
+ // - anything else (no state / error / no watermark yet): full re-scan from 0.
156
+ let sinceIso = null;
164
157
  if (resume) {
165
158
  const { data: state } = await supabase
166
159
  .from("floricode_sync_state")
167
160
  .select("cursor_skip,last_change_date_time,status")
168
161
  .eq("resource", REG_FEATURE_TYPE_RESOURCE)
169
162
  .maybeSingle();
170
- if (state && state.status === "running") {
171
- skip = Number(state.cursor_skip ?? 0) || 0;
172
- maxChangeDateTime = state.last_change_date_time ?? null;
163
+ const s = state;
164
+ if (s?.status === "running") {
165
+ skip = Number(s.cursor_skip ?? 0) || 0;
166
+ maxChangeDateTime = s.last_change_date_time ?? null;
167
+ sinceIso = s.last_change_date_time ?? null;
168
+ }
169
+ else if (s?.status === "ok" && s.last_change_date_time) {
170
+ sinceIso = s.last_change_date_time;
171
+ maxChangeDateTime = s.last_change_date_time;
173
172
  }
174
173
  }
175
174
  const startSkip = skip;
175
+ const incremental = Boolean(sinceIso);
176
176
  let fetched = 0;
177
177
  let upserted = 0;
178
178
  let batches = 0;
@@ -181,12 +181,13 @@ export async function syncFloricodeRegulatoryFeatures(supabase, options = {}) {
181
181
  status: "running",
182
182
  error_message: null,
183
183
  cursor_skip: skip,
184
+ last_change_date_time: maxChangeDateTime,
184
185
  last_sync_at: new Date().toISOString(),
185
186
  });
186
187
  try {
187
188
  const token = await getFloricodeToken(REG_FEATURE_TYPE_SCOPE);
188
189
  while (batches < maxBatches) {
189
- const items = await fetchRegFeatureTypePage(token, skip, batchSize);
190
+ const items = await fetchRegFeatureTypePage(token, skip, batchSize, sinceIso);
190
191
  if (items.length === 0) {
191
192
  completed = true;
192
193
  break;
@@ -208,19 +209,28 @@ export async function syncFloricodeRegulatoryFeatures(supabase, options = {}) {
208
209
  last_change_date_time: maxChangeDateTime,
209
210
  last_sync_at: new Date().toISOString(),
210
211
  });
211
- if (items.length < batchSize) {
212
- completed = true;
213
- break;
214
- }
215
- }
216
- // Ook bij partial runs projectie draaien zodat vbn-product-characteristic
217
- // meegroeit met de raw cache. Fouten hier mogen de cursor niet blokkeren.
218
- if (!completed && upserted > 0) {
212
+ // Lichte per-product projectie: alleen de zojuist geraakte producten
213
+ // opnieuw projecteren i.p.v. de volledige (200k+) rebuild na elke batch.
214
+ // Voorkomt "canceling statement due to statement timeout" tijdens partial syncs.
219
215
  try {
220
- await supabase.rpc("refresh_floricode_derived_code_lists");
216
+ const productIds = Array.from(new Set(items
217
+ .map((i) => (i?.product_id != null ? String(i.product_id) : null))
218
+ .filter((v) => Boolean(v))));
219
+ if (productIds.length > 0) {
220
+ // Chunk om payload-limieten en planner-blowups te vermijden.
221
+ for (const part of chunk(productIds, 500)) {
222
+ await supabase.rpc("refresh_floricode_derived_for_products", {
223
+ _product_ids: part,
224
+ });
225
+ }
226
+ }
221
227
  }
222
228
  catch (e) {
223
- console.warn("[regulatory] partial projection skipped:", String(e?.message ?? e));
229
+ console.warn("[regulatory] partial per-product projection skipped:", String(e?.message ?? e));
230
+ }
231
+ if (items.length < batchSize) {
232
+ completed = true;
233
+ break;
224
234
  }
225
235
  }
226
236
  if (completed) {
@@ -235,7 +245,7 @@ export async function syncFloricodeRegulatoryFeatures(supabase, options = {}) {
235
245
  if (projectionError)
236
246
  throw new Error(`Projectie naar code-lists mislukt: ${projectionError.message}`);
237
247
  await setSyncState(supabase, REG_FEATURE_TYPE_RESOURCE, {
238
- status: "success",
248
+ status: "ok",
239
249
  error_message: null,
240
250
  cursor_skip: 0,
241
251
  last_sequence_number: skip,
@@ -0,0 +1,35 @@
1
+ export interface FloricodeResourceDef {
2
+ resource: string;
3
+ endpoint: string;
4
+ scope: string;
5
+ /** Columns forming the primary key; order used for $orderby unique sort. */
6
+ keyColumns: string[];
7
+ }
8
+ export declare const FLORICODE_RESOURCES: FloricodeResourceDef[];
9
+ export interface FloricodeSyncResult {
10
+ ok: true;
11
+ resource: string;
12
+ fetched: number;
13
+ upserted: number;
14
+ batches: number;
15
+ finished: boolean;
16
+ nextWatermark: string | null;
17
+ durationMs: number;
18
+ }
19
+ export declare function syncFloricodeResource(resource: string, opts?: {
20
+ batchSize?: number;
21
+ maxBatches?: number;
22
+ resume?: boolean;
23
+ }): Promise<FloricodeSyncResult>;
24
+ export declare function syncAllFloricodeResources(opts: {
25
+ batchSize?: number;
26
+ maxBatchesPerResource?: number;
27
+ }): Promise<{
28
+ ok: true;
29
+ results: Array<FloricodeSyncResult | {
30
+ resource: string;
31
+ ok: false;
32
+ error: string;
33
+ }>;
34
+ }>;
35
+ //# sourceMappingURL=floricode-sync.server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"floricode-sync.server.d.ts","sourceRoot":"","sources":["../../src/lib/floricode-sync.server.ts"],"names":[],"mappings":"AAiBA,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,eAAO,MAAM,mBAAmB,EAAE,oBAAoB,EAkBrD,CAAC;AA6EF,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,IAAI,CAAC;IACT,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;IAClB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACvE,OAAO,CAAC,mBAAmB,CAAC,CAmH9B;AAED,wBAAsB,yBAAyB,CAAC,IAAI,EAAE;IACpD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,KAAK,CAAC,mBAAmB,GAAG;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAAE,CAAC,CAc9G"}