@egen-civitas/esm-platform 0.0.0-stage → 1.2.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.
- package/CHANGELOG.md +29 -0
- package/README.md +117 -2
- package/dist/adapters/web/camera.d.ts +10 -0
- package/dist/adapters/web/camera.js +31 -0
- package/dist/adapters/web/file-registry.d.ts +7 -0
- package/dist/adapters/web/file-registry.js +26 -0
- package/dist/adapters/web/files.d.ts +7 -0
- package/dist/adapters/web/files.js +22 -0
- package/dist/adapters/web/index.d.ts +9 -0
- package/dist/adapters/web/index.js +9 -0
- package/dist/adapters/web/lifecycle.d.ts +3 -0
- package/dist/adapters/web/lifecycle.js +20 -0
- package/dist/adapters/web/open-external.d.ts +8 -0
- package/dist/adapters/web/open-external.js +32 -0
- package/dist/adapters/web/permissions.d.ts +12 -0
- package/dist/adapters/web/permissions.js +48 -0
- package/dist/adapters/web/pick-files.d.ts +17 -0
- package/dist/adapters/web/pick-files.js +49 -0
- package/dist/adapters/web/session.d.ts +12 -0
- package/dist/adapters/web/session.js +19 -0
- package/dist/adapters/web/web-adapter.test.js +1233 -0
- package/dist/bridge/client.d.ts +48 -0
- package/dist/bridge/client.js +212 -0
- package/dist/bridge/client.test.js +1264 -0
- package/dist/bridge/index.d.ts +4 -0
- package/dist/bridge/index.js +2 -0
- package/dist/bridge/protocol.d.ts +72 -0
- package/dist/bridge/protocol.js +66 -0
- package/dist/bridge/protocol.test.js +215 -0
- package/dist/capabilities.d.ts +34 -0
- package/dist/capabilities.js +48 -0
- package/dist/detect.d.ts +25 -0
- package/dist/detect.js +38 -0
- package/dist/detect.test.js +23 -0
- package/dist/errors.d.ts +27 -0
- package/dist/errors.js +27 -0
- package/dist/errors.test.js +31 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +14 -0
- package/dist/native-platform.test.js +329 -0
- package/dist/native.d.ts +24 -0
- package/dist/native.js +46 -0
- package/dist/platform.d.ts +24 -0
- package/dist/platform.js +56 -0
- package/dist/platform.test.js +696 -0
- package/dist/register-capabilities.test.js +188 -0
- package/dist/registry.d.ts +52 -0
- package/dist/registry.js +101 -0
- package/dist/registry.test.js +681 -0
- package/dist/testing/index.d.ts +23 -0
- package/dist/testing/index.js +14 -0
- package/dist/testing/mock-host.d.ts +49 -0
- package/dist/testing/mock-host.js +86 -0
- package/dist/types.d.ts +108 -0
- package/dist/types.js +1 -0
- package/package.json +57 -3
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { createBridgeClient } from './client.js';
|
|
2
|
+
export type { BridgeClient, BridgeClientOptions, CallOptions, HostInfo } from './client.js';
|
|
3
|
+
export { DEFAULT_TIMEOUT_MS, HOST_PROTOCOL_NAME, MAX_MESSAGE_LENGTH, SUPPORTED_PROTOCOL, isSupportedProtocolVersion, parseHostMessage, } from './protocol.js';
|
|
4
|
+
export type { HostBridge, HostEvent, HostFailure, HostMessage, HostRequest, HostSuccess } from './protocol.js';
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Protocole Host ↔ Shell, version 1 — voir docs/EGEN-FRAMEWORK-DEV.md, annexe 1.
|
|
3
|
+
* Transport : chaînes JSON UTF-8 via `window.egenHostBridge`.
|
|
4
|
+
*/
|
|
5
|
+
/** Valeur du champ `p` de chaque message : tout message sans elle est ignoré. */
|
|
6
|
+
export declare const HOST_PROTOCOL_NAME = "egen-host";
|
|
7
|
+
/** Versions du protocole que CE client sait parler. Un Host hors de cette plage est `INCOMPATIBLE`. */
|
|
8
|
+
export declare const SUPPORTED_PROTOCOL: {
|
|
9
|
+
readonly min: 1;
|
|
10
|
+
readonly max: 1;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Taille maximale d'un message JSON, en caractères (proposition de la doc, à valider sur appareil). Au-delà, le Host
|
|
14
|
+
* doit fournir une référence de fichier (`FileHandle`) plutôt qu'un binaire inline.
|
|
15
|
+
*/
|
|
16
|
+
export declare const MAX_MESSAGE_LENGTH: number;
|
|
17
|
+
/** Délai par défaut d'un appel avant `TIMEOUT`. */
|
|
18
|
+
export declare const DEFAULT_TIMEOUT_MS = 30000;
|
|
19
|
+
/**
|
|
20
|
+
* Contrat de pont que le Host injecte AVANT tout script de la page : `window.egenHostBridge`.
|
|
21
|
+
* Le framework ne connaît rien d'autre du natif (règles R2 / N2).
|
|
22
|
+
*/
|
|
23
|
+
export interface HostBridge {
|
|
24
|
+
/** web → host : un message JSON. */
|
|
25
|
+
send(json: string): void;
|
|
26
|
+
/** host → web : le Host appelle `callback` avec un message JSON. Appelé UNE fois par le client. */
|
|
27
|
+
subscribe(callback: (json: string) => void): void;
|
|
28
|
+
}
|
|
29
|
+
export interface HostRequest {
|
|
30
|
+
p: typeof HOST_PROTOCOL_NAME;
|
|
31
|
+
v: number;
|
|
32
|
+
id: string;
|
|
33
|
+
ns: string;
|
|
34
|
+
m: string;
|
|
35
|
+
a?: unknown;
|
|
36
|
+
}
|
|
37
|
+
export interface HostSuccess {
|
|
38
|
+
p: typeof HOST_PROTOCOL_NAME;
|
|
39
|
+
v: number;
|
|
40
|
+
id: string;
|
|
41
|
+
ok: true;
|
|
42
|
+
r?: unknown;
|
|
43
|
+
}
|
|
44
|
+
export interface HostFailure {
|
|
45
|
+
p: typeof HOST_PROTOCOL_NAME;
|
|
46
|
+
v: number;
|
|
47
|
+
id: string;
|
|
48
|
+
ok: false;
|
|
49
|
+
/** Contenu NON fiable : validé par le client avant d'être converti en `PlatformError`. */
|
|
50
|
+
e: {
|
|
51
|
+
code?: unknown;
|
|
52
|
+
message?: unknown;
|
|
53
|
+
retryable?: unknown;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export interface HostEvent {
|
|
57
|
+
p: typeof HOST_PROTOCOL_NAME;
|
|
58
|
+
v: number;
|
|
59
|
+
ev: string;
|
|
60
|
+
d?: unknown;
|
|
61
|
+
}
|
|
62
|
+
export type HostMessage = HostSuccess | HostFailure | HostEvent;
|
|
63
|
+
export declare const KNOWN_ERROR_CODES: ReadonlySet<string>;
|
|
64
|
+
export declare function isSupportedProtocolVersion(version: unknown): version is number;
|
|
65
|
+
/**
|
|
66
|
+
* Décode un message du Host. Retourne `null` pour tout ce qui n'est pas un message du protocole : entrée non
|
|
67
|
+
* textuelle, trop longue, JSON invalide, autre valeur de `p`, forme inconnue. Ne lève JAMAIS et n'évalue rien.
|
|
68
|
+
*
|
|
69
|
+
* Le champ `v` n'est PAS filtré ici : le client doit pouvoir répondre « incompatible » à un appel en attente plutôt
|
|
70
|
+
* que de le laisser expirer.
|
|
71
|
+
*/
|
|
72
|
+
export declare function parseHostMessage(raw: unknown): HostMessage | null;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Protocole Host ↔ Shell, version 1 — voir docs/EGEN-FRAMEWORK-DEV.md, annexe 1.
|
|
3
|
+
* Transport : chaînes JSON UTF-8 via `window.egenHostBridge`.
|
|
4
|
+
*/
|
|
5
|
+
/** Valeur du champ `p` de chaque message : tout message sans elle est ignoré. */
|
|
6
|
+
export const HOST_PROTOCOL_NAME = 'egen-host';
|
|
7
|
+
/** Versions du protocole que CE client sait parler. Un Host hors de cette plage est `INCOMPATIBLE`. */
|
|
8
|
+
export const SUPPORTED_PROTOCOL = { min: 1, max: 1 };
|
|
9
|
+
/**
|
|
10
|
+
* Taille maximale d'un message JSON, en caractères (proposition de la doc, à valider sur appareil). Au-delà, le Host
|
|
11
|
+
* doit fournir une référence de fichier (`FileHandle`) plutôt qu'un binaire inline.
|
|
12
|
+
*/
|
|
13
|
+
export const MAX_MESSAGE_LENGTH = 256 * 1024;
|
|
14
|
+
/** Délai par défaut d'un appel avant `TIMEOUT`. */
|
|
15
|
+
export const DEFAULT_TIMEOUT_MS = 30000;
|
|
16
|
+
export const KNOWN_ERROR_CODES = new Set([
|
|
17
|
+
'UNAVAILABLE',
|
|
18
|
+
'DENIED',
|
|
19
|
+
'CANCELLED',
|
|
20
|
+
'INVALID_ARGS',
|
|
21
|
+
'TIMEOUT',
|
|
22
|
+
'INCOMPATIBLE',
|
|
23
|
+
'HOST_ERROR',
|
|
24
|
+
]);
|
|
25
|
+
export function isSupportedProtocolVersion(version) {
|
|
26
|
+
return (Number.isInteger(version) &&
|
|
27
|
+
version >= SUPPORTED_PROTOCOL.min &&
|
|
28
|
+
version <= SUPPORTED_PROTOCOL.max);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Décode un message du Host. Retourne `null` pour tout ce qui n'est pas un message du protocole : entrée non
|
|
32
|
+
* textuelle, trop longue, JSON invalide, autre valeur de `p`, forme inconnue. Ne lève JAMAIS et n'évalue rien.
|
|
33
|
+
*
|
|
34
|
+
* Le champ `v` n'est PAS filtré ici : le client doit pouvoir répondre « incompatible » à un appel en attente plutôt
|
|
35
|
+
* que de le laisser expirer.
|
|
36
|
+
*/
|
|
37
|
+
export function parseHostMessage(raw) {
|
|
38
|
+
if (typeof raw !== 'string' || raw.length === 0 || raw.length > MAX_MESSAGE_LENGTH) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
let value;
|
|
42
|
+
try {
|
|
43
|
+
value = JSON.parse(raw);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const message = value;
|
|
52
|
+
if (message.p !== HOST_PROTOCOL_NAME || typeof message.v !== 'number') {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
if (typeof message.id === 'string' && typeof message.ok === 'boolean') {
|
|
56
|
+
if (message.ok) {
|
|
57
|
+
return { p: HOST_PROTOCOL_NAME, v: message.v, id: message.id, ok: true, r: message.r };
|
|
58
|
+
}
|
|
59
|
+
const e = typeof message.e === 'object' && message.e !== null && !Array.isArray(message.e) ? message.e : {};
|
|
60
|
+
return { p: HOST_PROTOCOL_NAME, v: message.v, id: message.id, ok: false, e };
|
|
61
|
+
}
|
|
62
|
+
if (typeof message.ev === 'string') {
|
|
63
|
+
return { p: HOST_PROTOCOL_NAME, v: message.v, ev: message.ev, d: message.d };
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
function _define_property(obj, key, value) {
|
|
2
|
+
if (key in obj) {
|
|
3
|
+
Object.defineProperty(obj, key, {
|
|
4
|
+
value: value,
|
|
5
|
+
enumerable: true,
|
|
6
|
+
configurable: true,
|
|
7
|
+
writable: true
|
|
8
|
+
});
|
|
9
|
+
} else {
|
|
10
|
+
obj[key] = value;
|
|
11
|
+
}
|
|
12
|
+
return obj;
|
|
13
|
+
}
|
|
14
|
+
function _object_spread(target) {
|
|
15
|
+
for(var i = 1; i < arguments.length; i++){
|
|
16
|
+
var source = arguments[i] != null ? arguments[i] : {};
|
|
17
|
+
var ownKeys = Object.keys(source);
|
|
18
|
+
if (typeof Object.getOwnPropertySymbols === "function") {
|
|
19
|
+
ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function(sym) {
|
|
20
|
+
return Object.getOwnPropertyDescriptor(source, sym).enumerable;
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
ownKeys.forEach(function(key) {
|
|
24
|
+
_define_property(target, key, source[key]);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return target;
|
|
28
|
+
}
|
|
29
|
+
import { describe, expect, it } from 'vitest';
|
|
30
|
+
import { MAX_MESSAGE_LENGTH, isSupportedProtocolVersion, parseHostMessage } from './protocol';
|
|
31
|
+
var msg = function msg(extra) {
|
|
32
|
+
return JSON.stringify(_object_spread({
|
|
33
|
+
p: 'egen-host',
|
|
34
|
+
v: 1
|
|
35
|
+
}, extra));
|
|
36
|
+
};
|
|
37
|
+
describe('isSupportedProtocolVersion', function() {
|
|
38
|
+
it('accepte la v1 uniquement', function() {
|
|
39
|
+
expect(isSupportedProtocolVersion(1)).toBe(true);
|
|
40
|
+
expect(isSupportedProtocolVersion(0)).toBe(false);
|
|
41
|
+
expect(isSupportedProtocolVersion(2)).toBe(false);
|
|
42
|
+
expect(isSupportedProtocolVersion(1.5)).toBe(false);
|
|
43
|
+
expect(isSupportedProtocolVersion('1')).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
describe('parseHostMessage', function() {
|
|
47
|
+
it('décode une réponse de succès', function() {
|
|
48
|
+
expect(parseHostMessage(msg({
|
|
49
|
+
id: 'a',
|
|
50
|
+
ok: true,
|
|
51
|
+
r: 42
|
|
52
|
+
}))).toEqual({
|
|
53
|
+
p: 'egen-host',
|
|
54
|
+
v: 1,
|
|
55
|
+
id: 'a',
|
|
56
|
+
ok: true,
|
|
57
|
+
r: 42
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
it("décode une réponse d'échec et normalise un `e` absent en objet vide", function() {
|
|
61
|
+
expect(parseHostMessage(msg({
|
|
62
|
+
id: 'a',
|
|
63
|
+
ok: false,
|
|
64
|
+
e: {
|
|
65
|
+
code: 'DENIED'
|
|
66
|
+
}
|
|
67
|
+
}))).toMatchObject({
|
|
68
|
+
ok: false,
|
|
69
|
+
e: {
|
|
70
|
+
code: 'DENIED'
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
expect(parseHostMessage(msg({
|
|
74
|
+
id: 'a',
|
|
75
|
+
ok: false
|
|
76
|
+
}))).toMatchObject({
|
|
77
|
+
ok: false,
|
|
78
|
+
e: {}
|
|
79
|
+
});
|
|
80
|
+
expect(parseHostMessage(msg({
|
|
81
|
+
id: 'a',
|
|
82
|
+
ok: false,
|
|
83
|
+
e: 'x'
|
|
84
|
+
}))).toMatchObject({
|
|
85
|
+
ok: false,
|
|
86
|
+
e: {}
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
it('décode un événement', function() {
|
|
90
|
+
expect(parseHostMessage(msg({
|
|
91
|
+
ev: 'lifecycle.resume',
|
|
92
|
+
d: {
|
|
93
|
+
a: 1
|
|
94
|
+
}
|
|
95
|
+
}))).toEqual({
|
|
96
|
+
p: 'egen-host',
|
|
97
|
+
v: 1,
|
|
98
|
+
ev: 'lifecycle.resume',
|
|
99
|
+
d: {
|
|
100
|
+
a: 1
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
it('ne filtre pas la version (le client répond « incompatible » plutôt que de laisser expirer)', function() {
|
|
105
|
+
expect(parseHostMessage(JSON.stringify({
|
|
106
|
+
p: 'egen-host',
|
|
107
|
+
v: 9,
|
|
108
|
+
id: 'a',
|
|
109
|
+
ok: true
|
|
110
|
+
}))).toMatchObject({
|
|
111
|
+
v: 9
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
it.each([
|
|
115
|
+
[
|
|
116
|
+
'non textuel',
|
|
117
|
+
42
|
|
118
|
+
],
|
|
119
|
+
[
|
|
120
|
+
'objet',
|
|
121
|
+
{}
|
|
122
|
+
],
|
|
123
|
+
[
|
|
124
|
+
'null',
|
|
125
|
+
null
|
|
126
|
+
],
|
|
127
|
+
[
|
|
128
|
+
'chaîne vide',
|
|
129
|
+
''
|
|
130
|
+
],
|
|
131
|
+
[
|
|
132
|
+
'JSON invalide',
|
|
133
|
+
'{pas du json'
|
|
134
|
+
],
|
|
135
|
+
[
|
|
136
|
+
'tableau',
|
|
137
|
+
'[]'
|
|
138
|
+
],
|
|
139
|
+
[
|
|
140
|
+
'nombre JSON',
|
|
141
|
+
'12'
|
|
142
|
+
],
|
|
143
|
+
[
|
|
144
|
+
'sans p',
|
|
145
|
+
JSON.stringify({
|
|
146
|
+
v: 1,
|
|
147
|
+
id: 'a',
|
|
148
|
+
ok: true
|
|
149
|
+
})
|
|
150
|
+
],
|
|
151
|
+
[
|
|
152
|
+
'mauvais p',
|
|
153
|
+
JSON.stringify({
|
|
154
|
+
p: 'autre',
|
|
155
|
+
v: 1,
|
|
156
|
+
id: 'a',
|
|
157
|
+
ok: true
|
|
158
|
+
})
|
|
159
|
+
],
|
|
160
|
+
[
|
|
161
|
+
'v non numérique',
|
|
162
|
+
JSON.stringify({
|
|
163
|
+
p: 'egen-host',
|
|
164
|
+
v: '1',
|
|
165
|
+
id: 'a',
|
|
166
|
+
ok: true
|
|
167
|
+
})
|
|
168
|
+
],
|
|
169
|
+
[
|
|
170
|
+
'ni réponse ni événement',
|
|
171
|
+
msg({
|
|
172
|
+
foo: 1
|
|
173
|
+
})
|
|
174
|
+
],
|
|
175
|
+
[
|
|
176
|
+
'id non textuel',
|
|
177
|
+
msg({
|
|
178
|
+
id: 5,
|
|
179
|
+
ok: true
|
|
180
|
+
})
|
|
181
|
+
],
|
|
182
|
+
[
|
|
183
|
+
'ok non booléen',
|
|
184
|
+
msg({
|
|
185
|
+
id: 'a',
|
|
186
|
+
ok: 'oui'
|
|
187
|
+
})
|
|
188
|
+
],
|
|
189
|
+
[
|
|
190
|
+
'ev non textuel',
|
|
191
|
+
msg({
|
|
192
|
+
ev: 5
|
|
193
|
+
})
|
|
194
|
+
]
|
|
195
|
+
])('ignore : %s', function(_label, raw) {
|
|
196
|
+
expect(parseHostMessage(raw)).toBeNull();
|
|
197
|
+
});
|
|
198
|
+
it('ignore un message trop long', function() {
|
|
199
|
+
expect(parseHostMessage(msg({
|
|
200
|
+
id: 'a',
|
|
201
|
+
ok: true,
|
|
202
|
+
r: 'x'.repeat(MAX_MESSAGE_LENGTH)
|
|
203
|
+
}))).toBeNull();
|
|
204
|
+
});
|
|
205
|
+
it("n'évalue rien : un contenu qui ressemble à du code reste une donnée", function() {
|
|
206
|
+
var parsed = parseHostMessage(msg({
|
|
207
|
+
id: 'a',
|
|
208
|
+
ok: true,
|
|
209
|
+
r: 'alert(1)//</script>'
|
|
210
|
+
}));
|
|
211
|
+
expect(parsed).toMatchObject({
|
|
212
|
+
r: 'alert(1)//</script>'
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { CapabilityName } from '@egen-civitas/esm-globals';
|
|
2
|
+
import type { CapabilityInterfaces, Platform, PlatformEnvironment } from './types.js';
|
|
3
|
+
/** Les capacités d'une plateforme, indexées par leur nom (clés de `CapabilityInterfaces`). */
|
|
4
|
+
export type CapabilityMap = {
|
|
5
|
+
[K in keyof CapabilityInterfaces]: CapabilityInterfaces[K];
|
|
6
|
+
};
|
|
7
|
+
/** Implémentations (éventuellement partielles) fournies par un adaptateur ; ce qui manque reste `UNAVAILABLE`. */
|
|
8
|
+
export type CapabilityOverrides = {
|
|
9
|
+
[K in keyof CapabilityInterfaces]?: Partial<CapabilityInterfaces[K]>;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Toutes les capacités DU REGISTRE, sous forme d'objets dont chaque méthode échoue en `UNAVAILABLE`. Une capacité
|
|
13
|
+
* absente EXISTE donc toujours comme objet (règle C2) ; c'est `has()` qui dit si elle est réellement fournie.
|
|
14
|
+
* Dérivé de `registry.ts` : ajouter une capacité au registre l'ajoute ici sans rien modifier.
|
|
15
|
+
*/
|
|
16
|
+
export declare function createUnavailableCapabilities(): CapabilityMap;
|
|
17
|
+
export interface AssemblePlatformOptions {
|
|
18
|
+
environment: PlatformEnvironment;
|
|
19
|
+
/** Capacités réellement fournies (adaptateur Web, client de pont natif, mock de test…). */
|
|
20
|
+
capabilities?: CapabilityOverrides;
|
|
21
|
+
/** Appelée à CHAQUE lecture de `platform.ready` (évite un rejet non observé si personne ne l'attend). */
|
|
22
|
+
ready: () => Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Filtre dynamique en plus de la présence d'un adaptateur : une capacité n'est disponible que si elle est fournie
|
|
25
|
+
* ET que ce prédicat l'accepte (natif : le Host l'a annoncée au handshake). Défaut : toujours vrai.
|
|
26
|
+
*/
|
|
27
|
+
available?: (capability: CapabilityName) => boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Assemble un objet `Platform` : les capacités fournies remplacent celles indisponibles, et `has()` n'est vrai que pour
|
|
31
|
+
* elles. Point unique de construction, partagé par `getPlatform()`, les adaptateurs (C4, C5) et les mocks (C9) : ils
|
|
32
|
+
* ne peuvent donc pas diverger du contrat.
|
|
33
|
+
*/
|
|
34
|
+
export declare function assemblePlatform({ environment, capabilities, ready, available, }: AssemblePlatformOptions): Platform;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { unavailable } from './errors.js';
|
|
2
|
+
import { getCapabilityDefinition, listCapabilityNames } from './registry.js';
|
|
3
|
+
/** Méthode asynchrone d'une capacité absente : REJETTE `UNAVAILABLE` (jamais de `null` ambigu). */
|
|
4
|
+
const rejecting = (capability, method) => () => Promise.reject(unavailable(capability, method));
|
|
5
|
+
/**
|
|
6
|
+
* Méthode d'une capacité absente dont la signature n'est pas une `Promise` (abonnements `onResume`, `onMessage`) :
|
|
7
|
+
* lève `UNAVAILABLE` immédiatement, faute de pouvoir rejeter. L'appelant teste `platform.has(…)` avant de s'abonner.
|
|
8
|
+
*/
|
|
9
|
+
const throwing = (capability, method) => () => {
|
|
10
|
+
throw unavailable(capability, method);
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Toutes les capacités DU REGISTRE, sous forme d'objets dont chaque méthode échoue en `UNAVAILABLE`. Une capacité
|
|
14
|
+
* absente EXISTE donc toujours comme objet (règle C2) ; c'est `has()` qui dit si elle est réellement fournie.
|
|
15
|
+
* Dérivé de `registry.ts` : ajouter une capacité au registre l'ajoute ici sans rien modifier.
|
|
16
|
+
*/
|
|
17
|
+
export function createUnavailableCapabilities() {
|
|
18
|
+
const capabilities = {};
|
|
19
|
+
for (const name of listCapabilityNames()) {
|
|
20
|
+
const methods = getCapabilityDefinition(name)?.methods ?? {};
|
|
21
|
+
capabilities[name] = Object.fromEntries(Object.entries(methods).map(([method, kind]) => [
|
|
22
|
+
method,
|
|
23
|
+
kind === 'subscribe' ? throwing(name, method) : rejecting(name, method),
|
|
24
|
+
]));
|
|
25
|
+
}
|
|
26
|
+
return capabilities;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Assemble un objet `Platform` : les capacités fournies remplacent celles indisponibles, et `has()` n'est vrai que pour
|
|
30
|
+
* elles. Point unique de construction, partagé par `getPlatform()`, les adaptateurs (C4, C5) et les mocks (C9) : ils
|
|
31
|
+
* ne peuvent donc pas diverger du contrat.
|
|
32
|
+
*/
|
|
33
|
+
export function assemblePlatform({ environment, capabilities = {}, ready, available, }) {
|
|
34
|
+
const base = createUnavailableCapabilities();
|
|
35
|
+
const provided = new Set(Object.keys(capabilities));
|
|
36
|
+
const merged = {};
|
|
37
|
+
for (const name of Object.keys(base)) {
|
|
38
|
+
merged[name] = { ...base[name], ...capabilities[name] };
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
environment,
|
|
42
|
+
get ready() {
|
|
43
|
+
return ready();
|
|
44
|
+
},
|
|
45
|
+
has: (capability) => provided.has(capability) && (available?.(capability) ?? true),
|
|
46
|
+
...merged,
|
|
47
|
+
};
|
|
48
|
+
}
|
package/dist/detect.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { HostBridge } from './bridge/protocol.js';
|
|
2
|
+
import type { PlatformEnvironment } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Environnement d'exécution courant. Source UNIQUE : `window.egenTarget`, posé au build.
|
|
5
|
+
* Jamais de sniffing de User-Agent ni de détection de bibliothèque (règle N4).
|
|
6
|
+
*
|
|
7
|
+
* @category Platform
|
|
8
|
+
*/
|
|
9
|
+
export declare function getPlatformEnvironment(): PlatformEnvironment;
|
|
10
|
+
/** `true` en Android/iOS. */
|
|
11
|
+
export declare function isNativePlatform(): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Le pont injecté par le Host (`window.egenHostBridge = { send, subscribe }`), ou `null` s'il est absent ou non
|
|
14
|
+
* conforme. Ne vérifie que la FORME du pont : le handshake (version, capacités) est fait par le client de pont.
|
|
15
|
+
* Jamais de détection par User-Agent ni par bibliothèque d'enveloppe (N4).
|
|
16
|
+
*
|
|
17
|
+
* @category Platform
|
|
18
|
+
*/
|
|
19
|
+
export declare function getHostBridge(): HostBridge | null;
|
|
20
|
+
/**
|
|
21
|
+
* `true` si le Host a injecté un pont conforme au contrat.
|
|
22
|
+
*
|
|
23
|
+
* @category Platform
|
|
24
|
+
*/
|
|
25
|
+
export declare function hasHostBridge(): boolean;
|
package/dist/detect.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { getEgenTarget, isNativeTarget } from '@egen-civitas/esm-globals';
|
|
2
|
+
/**
|
|
3
|
+
* Environnement d'exécution courant. Source UNIQUE : `window.egenTarget`, posé au build.
|
|
4
|
+
* Jamais de sniffing de User-Agent ni de détection de bibliothèque (règle N4).
|
|
5
|
+
*
|
|
6
|
+
* @category Platform
|
|
7
|
+
*/
|
|
8
|
+
export function getPlatformEnvironment() {
|
|
9
|
+
return getEgenTarget();
|
|
10
|
+
}
|
|
11
|
+
/** `true` en Android/iOS. */
|
|
12
|
+
export function isNativePlatform() {
|
|
13
|
+
return isNativeTarget();
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Le pont injecté par le Host (`window.egenHostBridge = { send, subscribe }`), ou `null` s'il est absent ou non
|
|
17
|
+
* conforme. Ne vérifie que la FORME du pont : le handshake (version, capacités) est fait par le client de pont.
|
|
18
|
+
* Jamais de détection par User-Agent ni par bibliothèque d'enveloppe (N4).
|
|
19
|
+
*
|
|
20
|
+
* @category Platform
|
|
21
|
+
*/
|
|
22
|
+
export function getHostBridge() {
|
|
23
|
+
if (typeof window === 'undefined') {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const bridge = window.egenHostBridge;
|
|
27
|
+
return bridge && typeof bridge.send === 'function' && typeof bridge.subscribe === 'function'
|
|
28
|
+
? bridge
|
|
29
|
+
: null;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* `true` si le Host a injecté un pont conforme au contrat.
|
|
33
|
+
*
|
|
34
|
+
* @category Platform
|
|
35
|
+
*/
|
|
36
|
+
export function hasHostBridge() {
|
|
37
|
+
return getHostBridge() !== null;
|
|
38
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { getPlatformEnvironment, isNativePlatform } from './detect';
|
|
3
|
+
describe('détection de plateforme', function() {
|
|
4
|
+
afterEach(function() {
|
|
5
|
+
delete window.egenTarget;
|
|
6
|
+
});
|
|
7
|
+
it('web par défaut', function() {
|
|
8
|
+
expect(getPlatformEnvironment()).toBe('web');
|
|
9
|
+
expect(isNativePlatform()).toBe(false);
|
|
10
|
+
});
|
|
11
|
+
it('lit window.egenTarget (posé au build)', function() {
|
|
12
|
+
window.egenTarget = 'ios';
|
|
13
|
+
expect(getPlatformEnvironment()).toBe('ios');
|
|
14
|
+
expect(isNativePlatform()).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
it("n'utilise jamais le User-Agent", function() {
|
|
17
|
+
Object.defineProperty(navigator, 'userAgent', {
|
|
18
|
+
value: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0)',
|
|
19
|
+
configurable: true
|
|
20
|
+
});
|
|
21
|
+
expect(getPlatformEnvironment()).toBe('web');
|
|
22
|
+
});
|
|
23
|
+
});
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Codes d'erreur normalisés d'une capacité de plateforme (protocole Host v1, annexe 1 du document de développement). */
|
|
2
|
+
export type PlatformErrorCode = 'UNAVAILABLE' | 'DENIED' | 'CANCELLED' | 'INVALID_ARGS' | 'TIMEOUT' | 'INCOMPATIBLE' | 'HOST_ERROR';
|
|
3
|
+
export interface PlatformErrorOptions {
|
|
4
|
+
/** `true` si rejouer le même appel peut réussir (ex. TIMEOUT). */
|
|
5
|
+
retryable?: boolean;
|
|
6
|
+
/** Capacité concernée (ex. `secureStorage`). */
|
|
7
|
+
capability?: string;
|
|
8
|
+
/** Méthode concernée (ex. `get`). */
|
|
9
|
+
method?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Erreur typée de toute capacité de plateforme. Les capacités REJETTENT toujours avec cette classe :
|
|
13
|
+
* jamais de `null` ambigu, jamais d'`Error` générique, jamais de texte localisé (les apps traduisent).
|
|
14
|
+
*
|
|
15
|
+
* @category Platform
|
|
16
|
+
*/
|
|
17
|
+
export declare class PlatformError extends Error {
|
|
18
|
+
readonly code: PlatformErrorCode;
|
|
19
|
+
readonly retryable: boolean;
|
|
20
|
+
readonly capability?: string;
|
|
21
|
+
readonly method?: string;
|
|
22
|
+
constructor(code: PlatformErrorCode, message: string, options?: PlatformErrorOptions);
|
|
23
|
+
}
|
|
24
|
+
/** Construit l'erreur standard d'une capacité indisponible dans l'environnement courant. */
|
|
25
|
+
export declare function unavailable(capability: string, method?: string): PlatformError;
|
|
26
|
+
/** Type guard : l'erreur vient-elle d'une capacité de plateforme ? */
|
|
27
|
+
export declare function isPlatformError(error: unknown): error is PlatformError;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Erreur typée de toute capacité de plateforme. Les capacités REJETTENT toujours avec cette classe :
|
|
3
|
+
* jamais de `null` ambigu, jamais d'`Error` générique, jamais de texte localisé (les apps traduisent).
|
|
4
|
+
*
|
|
5
|
+
* @category Platform
|
|
6
|
+
*/
|
|
7
|
+
export class PlatformError extends Error {
|
|
8
|
+
constructor(code, message, options = {}) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'PlatformError';
|
|
11
|
+
this.code = code;
|
|
12
|
+
this.retryable = options.retryable ?? false;
|
|
13
|
+
this.capability = options.capability;
|
|
14
|
+
this.method = options.method;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Construit l'erreur standard d'une capacité indisponible dans l'environnement courant. */
|
|
18
|
+
export function unavailable(capability, method) {
|
|
19
|
+
return new PlatformError('UNAVAILABLE', `La capacité "${capability}" n'est pas disponible dans cet environnement.`, {
|
|
20
|
+
capability,
|
|
21
|
+
method,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/** Type guard : l'erreur vient-elle d'une capacité de plateforme ? */
|
|
25
|
+
export function isPlatformError(error) {
|
|
26
|
+
return error instanceof PlatformError;
|
|
27
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { PlatformError, isPlatformError, unavailable } from './errors';
|
|
3
|
+
describe('PlatformError', function() {
|
|
4
|
+
it('porte code, retryable et contexte', function() {
|
|
5
|
+
var e = new PlatformError('TIMEOUT', 'trop long', {
|
|
6
|
+
retryable: true,
|
|
7
|
+
capability: 'files',
|
|
8
|
+
method: 'select'
|
|
9
|
+
});
|
|
10
|
+
expect(e).toBeInstanceOf(Error);
|
|
11
|
+
expect(e.name).toBe('PlatformError');
|
|
12
|
+
expect(e.code).toBe('TIMEOUT');
|
|
13
|
+
expect(e.retryable).toBe(true);
|
|
14
|
+
expect(e.capability).toBe('files');
|
|
15
|
+
expect(e.method).toBe('select');
|
|
16
|
+
});
|
|
17
|
+
it('retryable vaut false par défaut', function() {
|
|
18
|
+
expect(new PlatformError('DENIED', 'non').retryable).toBe(false);
|
|
19
|
+
});
|
|
20
|
+
it('unavailable() construit une erreur UNAVAILABLE non rejouable', function() {
|
|
21
|
+
var e = unavailable('secureStorage', 'get');
|
|
22
|
+
expect(e.code).toBe('UNAVAILABLE');
|
|
23
|
+
expect(e.retryable).toBe(false);
|
|
24
|
+
expect(e.capability).toBe('secureStorage');
|
|
25
|
+
});
|
|
26
|
+
it('isPlatformError distingue les erreurs de plateforme', function() {
|
|
27
|
+
expect(isPlatformError(unavailable('x'))).toBe(true);
|
|
28
|
+
expect(isPlatformError(new Error('x'))).toBe(false);
|
|
29
|
+
expect(isPlatformError(null)).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
});
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// @egen-civitas/esm-platform — Point d'entrée public
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Capacités de plateforme (web / Android / iOS) pour les apps EGEN.
|
|
5
|
+
// Ce package ne contient AUCUN code natif et ne dépend d'aucune bibliothèque d'enveloppe (Capacitor…) :
|
|
6
|
+
// il définit des contrats et parle à un Host par un protocole versionné.
|
|
7
|
+
// Voir docs/EGEN-FRAMEWORK-DEV.md (chantier C).
|
|
8
|
+
// ============================================================================
|
|
9
|
+
export * from './types.js';
|
|
10
|
+
export * from './errors.js';
|
|
11
|
+
export * from './detect.js';
|
|
12
|
+
export { getPlatform, registerPlatformCapabilities } from './platform.js';
|
|
13
|
+
export { getHostInfo } from './native.js';
|
|
14
|
+
export * from './bridge/index.js';
|