@supersoniks/concorde 4.7.0 → 4.7.4
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/ai/AGENTS.md +4 -0
- package/ai/cursor/rules/concorde.mdc +10 -0
- package/ai/jetbrains/rules/concorde.md +8 -0
- package/ai/skills/concorde/SKILL.md +83 -3
- package/build-infos.json +1 -1
- package/concorde-core.bundle.js +129 -129
- package/concorde-core.es.js +1043 -950
- package/dist/concorde-core.bundle.js +129 -129
- package/dist/concorde-core.es.js +1043 -950
- package/dist/robots.txt +2 -0
- package/docs/assets/{index-D9pxaQYK.js → index-CwtPzTFq.js} +203 -203
- package/docs/index.html +1 -1
- package/docs/robots.txt +2 -0
- package/docs/src/core/components/ui/captcha/captcha.md +0 -12
- package/docs/src/tsconfig.json +383 -338
- package/package.json +1 -1
- package/public/robots.txt +2 -0
- package/src/core/components/ui/captcha/captcha.md +0 -12
- package/src/core/decorators/api.ts +128 -15
- package/src/core/utils/HTML.ts +42 -10
- package/php/get-challenge.php +0 -34
- package/php/some-service.php +0 -42
package/package.json
CHANGED
|
@@ -10,15 +10,3 @@
|
|
|
10
10
|
</template>
|
|
11
11
|
</sonic-code>
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
<sonic-code>
|
|
16
|
-
<template>
|
|
17
|
-
<sonic-captcha formDataProvider="captchaTestDataProvider">
|
|
18
|
-
<sonic-submit serviceURL="https://atelier.julien.supersoniks.pro" endPoint="php/some-service.php" onclick>
|
|
19
|
-
<sonic-button class="mt-4">Submit with captcha</sonic-button>
|
|
20
|
-
</sonic-submit>
|
|
21
|
-
</sonic-captcha>
|
|
22
|
-
</template>
|
|
23
|
-
</sonic-code>
|
|
24
|
-
|
|
@@ -47,11 +47,110 @@ function resolveScopedConfiguration(
|
|
|
47
47
|
return HTML.getApiConfiguration(host);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
function isScopedConfigurationReady(
|
|
51
|
+
config: APIConfiguration | null,
|
|
52
|
+
): config is APIConfiguration {
|
|
53
|
+
return typeof config?.serviceURL === "string" && config.serviceURL.length > 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Attributs scope dont la présence peut déclencher un GET différé (Lit → minuscules). */
|
|
57
|
+
const SCOPE_WATCH_ATTRIBUTES = [
|
|
58
|
+
"serviceURL",
|
|
59
|
+
"serviceurl",
|
|
60
|
+
"token",
|
|
61
|
+
"credentials",
|
|
62
|
+
"tokenProvider",
|
|
63
|
+
"tokenprovider",
|
|
64
|
+
"userName",
|
|
65
|
+
"username",
|
|
66
|
+
"password",
|
|
67
|
+
"eventsApiToken",
|
|
68
|
+
"eventsapitoken",
|
|
69
|
+
] as const;
|
|
70
|
+
|
|
71
|
+
type LitHost = HTMLElement & { updateComplete?: Promise<unknown> };
|
|
72
|
+
|
|
73
|
+
function isLitHost(component: unknown): component is LitHost {
|
|
74
|
+
return (
|
|
75
|
+
component instanceof HTMLElement &&
|
|
76
|
+
typeof (component as LitHost).updateComplete !== "undefined"
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function scheduleAfterHostReady(
|
|
81
|
+
component: unknown,
|
|
82
|
+
callback: () => void,
|
|
83
|
+
): () => void {
|
|
84
|
+
if (isLitHost(component)) {
|
|
85
|
+
let cancelled = false;
|
|
86
|
+
void component.updateComplete!.then(() => {
|
|
87
|
+
if (!cancelled) callback();
|
|
88
|
+
});
|
|
89
|
+
return () => {
|
|
90
|
+
cancelled = true;
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
// HTMLElement classique : config ancêtre déjà lisible ; sinon watchScopedConfiguration.
|
|
94
|
+
callback();
|
|
95
|
+
return () => {};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Attend que la config scope soit lisible (attributs DOM ou propriétés Lit reflect).
|
|
100
|
+
*/
|
|
101
|
+
function watchScopedConfiguration(
|
|
102
|
+
component: unknown,
|
|
103
|
+
onReady: () => void,
|
|
104
|
+
): () => void {
|
|
105
|
+
const host = asSearchableHost(component);
|
|
106
|
+
if (!host) return () => {};
|
|
107
|
+
|
|
108
|
+
let settled = false;
|
|
109
|
+
const tryNotify = () => {
|
|
110
|
+
if (settled) return;
|
|
111
|
+
if (!isScopedConfigurationReady(resolveScopedConfiguration(component))) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
settled = true;
|
|
115
|
+
onReady();
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
tryNotify();
|
|
119
|
+
if (settled) return () => {};
|
|
120
|
+
|
|
121
|
+
const cleanups: Array<() => void> = [];
|
|
122
|
+
|
|
123
|
+
const rafId = requestAnimationFrame(() => tryNotify());
|
|
124
|
+
cleanups.push(() => cancelAnimationFrame(rafId));
|
|
125
|
+
|
|
126
|
+
queueMicrotask(() => tryNotify());
|
|
127
|
+
|
|
128
|
+
const observer = new MutationObserver(() => tryNotify());
|
|
129
|
+
let node: SearchableDomElement | null = host;
|
|
130
|
+
while (node) {
|
|
131
|
+
if (node instanceof Element) {
|
|
132
|
+
observer.observe(node, {
|
|
133
|
+
attributes: true,
|
|
134
|
+
attributeFilter: [...SCOPE_WATCH_ATTRIBUTES],
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
node = (node.parentNode ||
|
|
138
|
+
(node as ShadowRoot).host) as SearchableDomElement;
|
|
139
|
+
}
|
|
140
|
+
cleanups.push(() => observer.disconnect());
|
|
141
|
+
|
|
142
|
+
return () => {
|
|
143
|
+
settled = true;
|
|
144
|
+
cleanups.forEach((cleanup) => cleanup());
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
50
148
|
type ApiGetState = {
|
|
51
149
|
cleanupWatchers: Array<() => void>;
|
|
52
150
|
requestGeneration: number;
|
|
53
151
|
configPublisher: DataProvider | null;
|
|
54
152
|
configMutationHandler: (() => void) | null;
|
|
153
|
+
scopeWatchCleanup: (() => void) | null;
|
|
55
154
|
};
|
|
56
155
|
|
|
57
156
|
function detachConfigPublisher(state: ApiGetState): void {
|
|
@@ -68,7 +167,8 @@ function detachConfigPublisher(state: ApiGetState): void {
|
|
|
68
167
|
* Le path est un `Endpoint<T, Ue>` ; les placeholders `${nomPropriété}` sont résolus sur l'instance (`Ue` contraint l’hôte).
|
|
69
168
|
*
|
|
70
169
|
* **Scoped (défaut)** : `HTML.getApiConfiguration(host)` avec `host` = l’élément connecté
|
|
71
|
-
* (`HTMLElement` / `ShadowRoot`).
|
|
170
|
+
* (`HTMLElement` / `ShadowRoot`). Si `serviceURL` n’est pas encore disponible (reflect Lit, scope
|
|
171
|
+
* ancêtre), le GET est différé jusqu’à ce que la config soit prête.
|
|
72
172
|
*
|
|
73
173
|
* **Deuxième paramètre** : `DataProviderKey<APIConfiguration>` — la config est lue via
|
|
74
174
|
* `PublisherManager` sur le chemin résolu (même syntaxe dynamique que `@subscribe`).
|
|
@@ -138,6 +238,7 @@ export function get<T, Ue = any, Uk = any>(
|
|
|
138
238
|
requestGeneration: 0,
|
|
139
239
|
configPublisher: null,
|
|
140
240
|
configMutationHandler: null,
|
|
241
|
+
scopeWatchCleanup: null,
|
|
141
242
|
};
|
|
142
243
|
comp[stateKey] = state;
|
|
143
244
|
}
|
|
@@ -166,8 +267,15 @@ export function get<T, Ue = any, Uk = any>(
|
|
|
166
267
|
} else {
|
|
167
268
|
config = resolveScopedConfiguration(component);
|
|
168
269
|
}
|
|
169
|
-
if (!config) {
|
|
170
|
-
|
|
270
|
+
if (!isScopedConfigurationReady(config)) {
|
|
271
|
+
if (!usesPublisherConfig && !state.scopeWatchCleanup) {
|
|
272
|
+
const scopeWatch = watchScopedConfiguration(component, runFetch);
|
|
273
|
+
state.scopeWatchCleanup = scopeWatch;
|
|
274
|
+
state.cleanupWatchers.push(() => {
|
|
275
|
+
scopeWatch();
|
|
276
|
+
state.scopeWatchCleanup = null;
|
|
277
|
+
});
|
|
278
|
+
}
|
|
171
279
|
return;
|
|
172
280
|
}
|
|
173
281
|
const generation = ++state.requestGeneration;
|
|
@@ -214,19 +322,24 @@ export function get<T, Ue = any, Uk = any>(
|
|
|
214
322
|
}
|
|
215
323
|
rebindPublisherConfig();
|
|
216
324
|
} else {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
325
|
+
const startScopedFetch = () => {
|
|
326
|
+
if (isDynamicPath) {
|
|
327
|
+
for (const dependency of endpointDynamicDependencies) {
|
|
328
|
+
const unsubscribe = observeDynamicProperty(
|
|
329
|
+
getDynamicWatchKeys.watcherStore,
|
|
330
|
+
getDynamicWatchKeys.hooked,
|
|
331
|
+
component,
|
|
332
|
+
dependency,
|
|
333
|
+
() => runFetch(),
|
|
334
|
+
);
|
|
335
|
+
state.cleanupWatchers.push(unsubscribe);
|
|
336
|
+
}
|
|
227
337
|
}
|
|
228
|
-
|
|
229
|
-
|
|
338
|
+
runFetch();
|
|
339
|
+
};
|
|
340
|
+
state.cleanupWatchers.push(
|
|
341
|
+
scheduleAfterHostReady(component, startScopedFetch),
|
|
342
|
+
);
|
|
230
343
|
}
|
|
231
344
|
});
|
|
232
345
|
|
package/src/core/utils/HTML.ts
CHANGED
|
@@ -59,8 +59,37 @@ class HTML {
|
|
|
59
59
|
return null;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/** Noms d'attribut DOM à tester (Lit reflect → minuscules, ex. serviceURL → serviceurl). */
|
|
63
|
+
private static scopeAttributeNames(attributeName: string): string[] {
|
|
64
|
+
const lower = attributeName.toLowerCase();
|
|
65
|
+
return lower === attributeName ? [attributeName] : [attributeName, lower];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private static readScopeValueOnElement(
|
|
69
|
+
element: HTMLElement,
|
|
70
|
+
attributeName: string,
|
|
71
|
+
): string | null {
|
|
72
|
+
// Lit : la propriété est fiable avant le reflect ; un attribut vide (serviceurl="")
|
|
73
|
+
// ne doit pas masquer la propriété.
|
|
74
|
+
const prop = (element as unknown as Record<string, unknown>)[attributeName];
|
|
75
|
+
if (typeof prop === "string" && prop.length > 0) {
|
|
76
|
+
return prop;
|
|
77
|
+
}
|
|
78
|
+
for (const attrName of HTML.scopeAttributeNames(attributeName)) {
|
|
79
|
+
if (element.hasAttribute(attrName)) {
|
|
80
|
+
const attr = element.getAttribute(attrName);
|
|
81
|
+
if (attr != null && attr.length > 0) {
|
|
82
|
+
return attr;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
62
89
|
/**
|
|
63
|
-
* Va de parent en parent en partant de node pour trouver un attribut
|
|
90
|
+
* Va de parent en parent en partant de node pour trouver un attribut.
|
|
91
|
+
* Si l'attribut n'est pas encore reflété (ex. Lit `@property({ reflect: true })`
|
|
92
|
+
* au premier `connectedCallback`), lit la propriété homonyme sur l'élément.
|
|
64
93
|
* @param attributeName nom de l'attribut
|
|
65
94
|
* @returns valeur de l'attribut ou null si l'attribut n'est pas trouvé
|
|
66
95
|
*/
|
|
@@ -69,16 +98,19 @@ class HTML {
|
|
|
69
98
|
attributeName: string
|
|
70
99
|
): string | null {
|
|
71
100
|
if (!node) return null;
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (
|
|
75
|
-
|
|
76
|
-
(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
101
|
+
let current: SearchableDomElement | null = node;
|
|
102
|
+
while (current) {
|
|
103
|
+
if (current instanceof HTMLElement) {
|
|
104
|
+
const value = HTML.readScopeValueOnElement(current, attributeName);
|
|
105
|
+
if (value != null && value.length > 0) {
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const parent = current.parentNode || (current as ShadowRoot).host;
|
|
110
|
+
if (!parent) break;
|
|
111
|
+
current = parent as SearchableDomElement;
|
|
80
112
|
}
|
|
81
|
-
return
|
|
113
|
+
return null;
|
|
82
114
|
}
|
|
83
115
|
|
|
84
116
|
/**
|
package/php/get-challenge.php
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
<?php
|
|
2
|
-
/* *
|
|
3
|
-
* Call get-challenge on auto-hosted latcha service at https://altcha.supersoniks.org
|
|
4
|
-
* */
|
|
5
|
-
|
|
6
|
-
// Autoriser toutes les origines
|
|
7
|
-
header("Access-Control-Allow-Origin: *");
|
|
8
|
-
|
|
9
|
-
// Autoriser les méthodes HTTP spécifiques
|
|
10
|
-
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
|
|
11
|
-
|
|
12
|
-
// Autoriser certains en-têtes spécifiques
|
|
13
|
-
header("Access-Control-Allow-Headers: Content-Type, Authorization");
|
|
14
|
-
|
|
15
|
-
// Si la méthode est OPTIONS, terminer la requête ici
|
|
16
|
-
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
|
17
|
-
http_response_code(200);
|
|
18
|
-
exit();
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function getChallenge($key){
|
|
22
|
-
$maxNumber=20000;
|
|
23
|
-
$queryString = $params = [
|
|
24
|
-
'key' => $key,
|
|
25
|
-
'maxNumber' => $maxNumber
|
|
26
|
-
];
|
|
27
|
-
// Générer la chaîne de requête
|
|
28
|
-
$queryString = http_build_query($params);
|
|
29
|
-
$url = "https://altcha.supersoniks.org/get-challenge?key=".$queryString;
|
|
30
|
-
$response = file_get_contents($url);
|
|
31
|
-
return $response;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
echo getChallenge($_GET['key']);
|
package/php/some-service.php
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
<?php
|
|
2
|
-
/* *
|
|
3
|
-
* Call verify-solution on auto-hosted latcha service at https://altcha.supersoniks.org
|
|
4
|
-
* */
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
// Autoriser toutes les origines
|
|
8
|
-
header("Access-Control-Allow-Origin: *");
|
|
9
|
-
|
|
10
|
-
// Autoriser les méthodes HTTP spécifiques
|
|
11
|
-
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
|
|
12
|
-
|
|
13
|
-
// Autoriser certains en-têtes spécifiques
|
|
14
|
-
header("Access-Control-Allow-Headers: Content-Type, Authorization, x-altcha-spam-filter");
|
|
15
|
-
|
|
16
|
-
// Si la méthode est OPTIONS, terminer la requête ici
|
|
17
|
-
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
|
18
|
-
http_response_code(200);
|
|
19
|
-
exit();
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function verifySolution($key, $solution){
|
|
23
|
-
$queryString = $params = [
|
|
24
|
-
'key' => $key,
|
|
25
|
-
'altcha' => $solution
|
|
26
|
-
];
|
|
27
|
-
// Générer la chaîne de requête
|
|
28
|
-
$queryString = http_build_query($params);
|
|
29
|
-
$url = "https://altcha.supersoniks.org/verify-solution?".$queryString;
|
|
30
|
-
$response = file_get_contents($url);
|
|
31
|
-
return $response;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Get json posted data
|
|
36
|
-
*/
|
|
37
|
-
|
|
38
|
-
// Get the posted data
|
|
39
|
-
|
|
40
|
-
$data = json_decode(file_get_contents("php://input"));
|
|
41
|
-
|
|
42
|
-
echo verifySolution($data->captchakey, $data->captchatoken);
|