@learncard/partner-connect 0.5.1 → 0.6.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/README.md +395 -83
- package/dist/index.d.ts +257 -25
- package/dist/partner-connect.esm.js +3294 -431
- package/dist/partner-connect.esm.js.map +1 -1
- package/dist/partner-connect.js +3295 -430
- package/dist/partner-connect.js.map +1 -1
- package/dist/partner-connect.mjs +3294 -431
- package/dist/partner-connect.mjs.map +1 -1
- package/package.json +3 -2
- package/src/docs-snippets.test.ts +79 -0
- package/src/index.ts +496 -33
- package/src/mock-host.ts +2447 -523
- package/src/mock-mode.test.ts +1337 -7
- package/src/origin-resolution.test.ts +229 -0
- package/src/types.ts +130 -11
package/dist/partner-connect.js
CHANGED
|
@@ -2,6 +2,1011 @@
|
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
|
+
const PREVIEW_HOST_SUFFIXES = [
|
|
6
|
+
".lovableproject.com",
|
|
7
|
+
// Lovable editor sandbox
|
|
8
|
+
".webcontainer-api.io",
|
|
9
|
+
// Bolt.new / StackBlitz WebContainers
|
|
10
|
+
".replit.dev",
|
|
11
|
+
// Replit development URLs (deployments use .replit.app)
|
|
12
|
+
".vusercontent.net"
|
|
13
|
+
// v0 generated previews (deployments use .vercel.app)
|
|
14
|
+
];
|
|
15
|
+
const LOVABLE_PREVIEW_PREFIX = "id-preview--";
|
|
16
|
+
const LOVABLE_APP_SUFFIX = ".lovable.app";
|
|
17
|
+
const EDITOR_HOSTS = [
|
|
18
|
+
"lovable.dev",
|
|
19
|
+
"bolt.new",
|
|
20
|
+
"stackblitz.com",
|
|
21
|
+
"v0.dev",
|
|
22
|
+
"v0.app",
|
|
23
|
+
"replit.com"
|
|
24
|
+
];
|
|
25
|
+
const isAppBuilderPreviewHost = (hostname) => {
|
|
26
|
+
const host = hostname.toLowerCase();
|
|
27
|
+
if (host.startsWith(LOVABLE_PREVIEW_PREFIX) && host.endsWith(LOVABLE_APP_SUFFIX)) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
return PREVIEW_HOST_SUFFIXES.some((suffix) => host.endsWith(suffix));
|
|
31
|
+
};
|
|
32
|
+
const isAppBuilderEditorOrigin = (origin) => {
|
|
33
|
+
try {
|
|
34
|
+
const { protocol, hostname } = new URL(origin);
|
|
35
|
+
if (protocol !== "https:") return false;
|
|
36
|
+
const host = hostname.toLowerCase().replace(/^www\./, "");
|
|
37
|
+
return EDITOR_HOSTS.includes(host);
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const WALLET_CATEGORIES = [
|
|
43
|
+
"Achievement",
|
|
44
|
+
"Skill",
|
|
45
|
+
"ID",
|
|
46
|
+
"Learning History",
|
|
47
|
+
"Work History",
|
|
48
|
+
"Social Badges",
|
|
49
|
+
"Membership",
|
|
50
|
+
"Accomplishment",
|
|
51
|
+
"Experiences",
|
|
52
|
+
"Accommodation",
|
|
53
|
+
"Family"
|
|
54
|
+
];
|
|
55
|
+
const PERSONAL_FIELDS = [
|
|
56
|
+
"name",
|
|
57
|
+
"email",
|
|
58
|
+
"phone",
|
|
59
|
+
"birthDate",
|
|
60
|
+
"country",
|
|
61
|
+
"avatar"
|
|
62
|
+
];
|
|
63
|
+
const RESERVED_TEMPLATE_VARIABLES = ["issue_date", "issuer_did", "recipient_did"];
|
|
64
|
+
const KNOWN_ACHIEVEMENT_TYPES = [
|
|
65
|
+
"Achievement",
|
|
66
|
+
"Assessment",
|
|
67
|
+
"Award",
|
|
68
|
+
"Badge",
|
|
69
|
+
"Certificate",
|
|
70
|
+
"Certification",
|
|
71
|
+
"CommunityService",
|
|
72
|
+
"Competency",
|
|
73
|
+
"Course",
|
|
74
|
+
"Degree",
|
|
75
|
+
"Diploma",
|
|
76
|
+
"LearningProgram",
|
|
77
|
+
"License",
|
|
78
|
+
"Membership",
|
|
79
|
+
"MicroCredential",
|
|
80
|
+
"ProfessionalDevelopment"
|
|
81
|
+
];
|
|
82
|
+
const VC2_CONTEXT = "https://www.w3.org/ns/credentials/v2";
|
|
83
|
+
const OBV3_CONTEXT = "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json";
|
|
84
|
+
const TEMPLATE_VARIABLE_REGEX = /\{\{(\w+)\}\}/g;
|
|
85
|
+
const VARIABLE_BRACES_REGEX = /\{\{([^{}]+)\}\}/g;
|
|
86
|
+
const BARE_VARIABLE_REGEX = /^\{\{(\w+)\}\}$/;
|
|
87
|
+
const CUSTOM_ACHIEVEMENT_TYPE_REGEX = /^ext:[A-Za-z0-9]+$/;
|
|
88
|
+
const isPlainObject = (value) => {
|
|
89
|
+
return Object.prototype.toString.call(value) === "[object Object]";
|
|
90
|
+
};
|
|
91
|
+
const isReservedTemplateVariable = (name) => {
|
|
92
|
+
return RESERVED_TEMPLATE_VARIABLES.includes(name);
|
|
93
|
+
};
|
|
94
|
+
const getBareVariableName = (value) => {
|
|
95
|
+
if (typeof value !== "string") {
|
|
96
|
+
return void 0;
|
|
97
|
+
}
|
|
98
|
+
const match = value.match(BARE_VARIABLE_REGEX);
|
|
99
|
+
return match == null ? void 0 : match[1];
|
|
100
|
+
};
|
|
101
|
+
const hasValidVariableInterpolation = (value) => {
|
|
102
|
+
TEMPLATE_VARIABLE_REGEX.lastIndex = 0;
|
|
103
|
+
return TEMPLATE_VARIABLE_REGEX.test(value);
|
|
104
|
+
};
|
|
105
|
+
const isIsoDateString = (value) => {
|
|
106
|
+
return !Number.isNaN(Date.parse(value));
|
|
107
|
+
};
|
|
108
|
+
const deepClone = (value) => {
|
|
109
|
+
if (Array.isArray(value)) {
|
|
110
|
+
return value.map((item) => deepClone(item));
|
|
111
|
+
}
|
|
112
|
+
if (isPlainObject(value)) {
|
|
113
|
+
const clone = {};
|
|
114
|
+
Object.keys(value).forEach((key) => {
|
|
115
|
+
clone[key] = deepClone(value[key]);
|
|
116
|
+
});
|
|
117
|
+
return clone;
|
|
118
|
+
}
|
|
119
|
+
return value;
|
|
120
|
+
};
|
|
121
|
+
const normalizeForJson = (value) => {
|
|
122
|
+
if (Array.isArray(value)) {
|
|
123
|
+
return value.map((item) => normalizeForJson(item));
|
|
124
|
+
}
|
|
125
|
+
if (isPlainObject(value)) {
|
|
126
|
+
const normalized = {};
|
|
127
|
+
Object.keys(value).sort((a, b) => a.localeCompare(b)).forEach((key) => {
|
|
128
|
+
const child = normalizeForJson(value[key]);
|
|
129
|
+
if (child !== void 0) {
|
|
130
|
+
normalized[key] = child;
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
return normalized;
|
|
134
|
+
}
|
|
135
|
+
return value;
|
|
136
|
+
};
|
|
137
|
+
const stripEmpty = (value) => {
|
|
138
|
+
if (Array.isArray(value)) {
|
|
139
|
+
const items = value.map((item) => stripEmpty(item)).filter((item) => item !== void 0);
|
|
140
|
+
return items.length > 0 ? items : void 0;
|
|
141
|
+
}
|
|
142
|
+
if (isPlainObject(value)) {
|
|
143
|
+
const next = {};
|
|
144
|
+
Object.keys(value).forEach((key) => {
|
|
145
|
+
const child = stripEmpty(value[key]);
|
|
146
|
+
if (child !== void 0) {
|
|
147
|
+
next[key] = child;
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
return Object.keys(next).length > 0 ? next : void 0;
|
|
151
|
+
}
|
|
152
|
+
return value === void 0 ? void 0 : value;
|
|
153
|
+
};
|
|
154
|
+
const levenshteinDistance = (a, b) => {
|
|
155
|
+
const rows = a.length + 1;
|
|
156
|
+
const cols = b.length + 1;
|
|
157
|
+
const dp = Array.from(
|
|
158
|
+
{ length: rows },
|
|
159
|
+
() => Array.from({ length: cols }, () => 0)
|
|
160
|
+
);
|
|
161
|
+
for (let row = 0; row < rows; row += 1) {
|
|
162
|
+
dp[row][0] = row;
|
|
163
|
+
}
|
|
164
|
+
for (let col = 0; col < cols; col += 1) {
|
|
165
|
+
dp[0][col] = col;
|
|
166
|
+
}
|
|
167
|
+
for (let row = 1; row < rows; row += 1) {
|
|
168
|
+
for (let col = 1; col < cols; col += 1) {
|
|
169
|
+
const cost = a[row - 1] === b[col - 1] ? 0 : 1;
|
|
170
|
+
dp[row][col] = Math.min(
|
|
171
|
+
dp[row - 1][col] + 1,
|
|
172
|
+
dp[row][col - 1] + 1,
|
|
173
|
+
dp[row - 1][col - 1] + cost
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return dp[a.length][b.length];
|
|
178
|
+
};
|
|
179
|
+
const getDidYouMean = (input, options) => {
|
|
180
|
+
if (options.length === 0) {
|
|
181
|
+
return void 0;
|
|
182
|
+
}
|
|
183
|
+
const normalizedInput = input.toLowerCase();
|
|
184
|
+
let bestOption;
|
|
185
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
186
|
+
options.forEach((option) => {
|
|
187
|
+
const normalizedOption = option.toLowerCase();
|
|
188
|
+
if (normalizedOption.startsWith(normalizedInput) || normalizedInput.startsWith(normalizedOption) || normalizedOption.includes(normalizedInput)) {
|
|
189
|
+
const prefixScore = 0;
|
|
190
|
+
if (prefixScore < bestScore) {
|
|
191
|
+
bestScore = prefixScore;
|
|
192
|
+
bestOption = option;
|
|
193
|
+
}
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const score = levenshteinDistance(normalizedInput, normalizedOption);
|
|
197
|
+
if (score < bestScore) {
|
|
198
|
+
bestScore = score;
|
|
199
|
+
bestOption = option;
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
if (bestOption === void 0) {
|
|
203
|
+
return void 0;
|
|
204
|
+
}
|
|
205
|
+
const threshold = Math.max(3, Math.floor(input.length / 2));
|
|
206
|
+
return bestScore <= threshold ? bestOption : void 0;
|
|
207
|
+
};
|
|
208
|
+
const validateAchievementType = (value) => {
|
|
209
|
+
if (KNOWN_ACHIEVEMENT_TYPES.includes(value) || CUSTOM_ACHIEVEMENT_TYPE_REGEX.test(value)) {
|
|
210
|
+
return void 0;
|
|
211
|
+
}
|
|
212
|
+
const suggestion = getDidYouMean(value, KNOWN_ACHIEVEMENT_TYPES);
|
|
213
|
+
const suggestionText = suggestion ? `; did you mean "${suggestion}"?` : "";
|
|
214
|
+
return `template.achievementType "${value}" is not a known OBv3 achievementType${suggestionText} Custom types must use the "ext:" prefix`;
|
|
215
|
+
};
|
|
216
|
+
const validateInterpolatedString = (value, path, errors) => {
|
|
217
|
+
VARIABLE_BRACES_REGEX.lastIndex = 0;
|
|
218
|
+
let match = VARIABLE_BRACES_REGEX.exec(value);
|
|
219
|
+
while (match) {
|
|
220
|
+
const variableName = match[1];
|
|
221
|
+
if (!/^\w+$/.test(variableName)) {
|
|
222
|
+
errors.push({
|
|
223
|
+
path,
|
|
224
|
+
message: `${path} uses invalid variable {{${variableName}}}; variable names must match /^\\w+$/`
|
|
225
|
+
});
|
|
226
|
+
} else if (isReservedTemplateVariable(variableName)) {
|
|
227
|
+
errors.push({
|
|
228
|
+
path,
|
|
229
|
+
message: `${path} uses reserved variable {{${variableName}}}; it is injected automatically by LearnCard`
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
match = VARIABLE_BRACES_REGEX.exec(value);
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
const validateWalletCategory = (value) => {
|
|
236
|
+
return typeof value === "string" && WALLET_CATEGORIES.includes(value);
|
|
237
|
+
};
|
|
238
|
+
const validatePersonalField = (value) => {
|
|
239
|
+
return typeof value === "string" && PERSONAL_FIELDS.includes(value);
|
|
240
|
+
};
|
|
241
|
+
const sortUnique = (values) => {
|
|
242
|
+
return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
|
|
243
|
+
};
|
|
244
|
+
const canonicalJsonString = (value) => {
|
|
245
|
+
return JSON.stringify(normalizeForJson(value));
|
|
246
|
+
};
|
|
247
|
+
const SIMPLE_TEMPLATE_KEYS = [
|
|
248
|
+
"name",
|
|
249
|
+
"description",
|
|
250
|
+
"image",
|
|
251
|
+
"issuerName",
|
|
252
|
+
"achievementType",
|
|
253
|
+
"criteria",
|
|
254
|
+
"alignments",
|
|
255
|
+
"tags",
|
|
256
|
+
"activity",
|
|
257
|
+
"credits",
|
|
258
|
+
"validUntil",
|
|
259
|
+
"evidence"
|
|
260
|
+
];
|
|
261
|
+
const pushError = (errors, path, message) => {
|
|
262
|
+
errors.push({ path, message });
|
|
263
|
+
};
|
|
264
|
+
const validateHttpsOrVariableUrl = (value, path, errors) => {
|
|
265
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
266
|
+
pushError(errors, path, `${path} must be a non-empty string`);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
validateInterpolatedString(value, path, errors);
|
|
270
|
+
if (!value.startsWith("https://") && !hasValidVariableInterpolation(value)) {
|
|
271
|
+
pushError(errors, path, `${path} must start with https:// or contain a {{variable}}`);
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
const validateTemplateDate = (value, path, errors) => {
|
|
275
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
276
|
+
pushError(errors, path, `${path} must be an ISO-8601 date or a {{variable}} token`);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
validateInterpolatedString(value, path, errors);
|
|
280
|
+
if (getBareVariableName(value) !== void 0 || isIsoDateString(value)) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
pushError(errors, path, `${path} must be an ISO-8601 date or a {{variable}} token`);
|
|
284
|
+
};
|
|
285
|
+
const validateTemplateNumber = (value, path, errors) => {
|
|
286
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (typeof value === "string") {
|
|
290
|
+
validateInterpolatedString(value, path, errors);
|
|
291
|
+
if (getBareVariableName(value) !== void 0) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
pushError(errors, path, `${path} must be a number or a {{variable}} token`);
|
|
296
|
+
};
|
|
297
|
+
const validateStringArray = (value, path, errors) => {
|
|
298
|
+
if (!Array.isArray(value)) {
|
|
299
|
+
pushError(errors, path, `${path} must be an array of strings`);
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
value.forEach((item, index) => {
|
|
303
|
+
if (typeof item !== "string") {
|
|
304
|
+
pushError(errors, `${path}[${index}]`, `${path}[${index}] must be a string`);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
validateInterpolatedString(item, `${path}[${index}]`, errors);
|
|
308
|
+
});
|
|
309
|
+
return true;
|
|
310
|
+
};
|
|
311
|
+
const validateWalletSkillTags = (value, path, errors) => {
|
|
312
|
+
if (value === void 0) {
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
if (!Array.isArray(value)) {
|
|
316
|
+
pushError(errors, path, `${path} must be an array`);
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
value.forEach((item, index) => {
|
|
320
|
+
const itemPath = `${path}[${index}]`;
|
|
321
|
+
if (!isPlainObject(item)) {
|
|
322
|
+
pushError(errors, itemPath, `${itemPath} must be an object`);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (typeof item.frameworkId !== "string" || item.frameworkId.length === 0) {
|
|
326
|
+
pushError(
|
|
327
|
+
errors,
|
|
328
|
+
`${itemPath}.frameworkId`,
|
|
329
|
+
`${itemPath}.frameworkId must be a non-empty string`
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
if (typeof item.id !== "string" || item.id.length === 0) {
|
|
333
|
+
pushError(errors, `${itemPath}.id`, `${itemPath}.id must be a non-empty string`);
|
|
334
|
+
}
|
|
335
|
+
if (item.proficiencyLevel !== void 0 && typeof item.proficiencyLevel !== "string") {
|
|
336
|
+
pushError(
|
|
337
|
+
errors,
|
|
338
|
+
`${itemPath}.proficiencyLevel`,
|
|
339
|
+
`${itemPath}.proficiencyLevel must be a string`
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
return true;
|
|
344
|
+
};
|
|
345
|
+
const validateAlignment = (value, path, errors) => {
|
|
346
|
+
if (!isPlainObject(value)) {
|
|
347
|
+
pushError(errors, path, `${path} must be an object`);
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
if (typeof value.name !== "string" || value.name.trim().length === 0) {
|
|
351
|
+
pushError(errors, `${path}.name`, `${path}.name is required`);
|
|
352
|
+
} else {
|
|
353
|
+
validateInterpolatedString(value.name, `${path}.name`, errors);
|
|
354
|
+
}
|
|
355
|
+
if (value.url === void 0) {
|
|
356
|
+
pushError(errors, `${path}.url`, `${path}.url is required`);
|
|
357
|
+
} else {
|
|
358
|
+
validateHttpsOrVariableUrl(value.url, `${path}.url`, errors);
|
|
359
|
+
}
|
|
360
|
+
if (value.framework !== void 0) {
|
|
361
|
+
if (typeof value.framework !== "string") {
|
|
362
|
+
pushError(errors, `${path}.framework`, `${path}.framework must be a string`);
|
|
363
|
+
} else {
|
|
364
|
+
validateInterpolatedString(value.framework, `${path}.framework`, errors);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (value.code !== void 0) {
|
|
368
|
+
if (typeof value.code !== "string") {
|
|
369
|
+
pushError(errors, `${path}.code`, `${path}.code must be a string`);
|
|
370
|
+
} else {
|
|
371
|
+
validateInterpolatedString(value.code, `${path}.code`, errors);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return true;
|
|
375
|
+
};
|
|
376
|
+
const validateEvidence = (value, path, errors) => {
|
|
377
|
+
if (!isPlainObject(value)) {
|
|
378
|
+
pushError(errors, path, `${path} must be an object`);
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
if (value.id !== void 0) {
|
|
382
|
+
validateHttpsOrVariableUrl(value.id, `${path}.id`, errors);
|
|
383
|
+
}
|
|
384
|
+
if (value.name !== void 0) {
|
|
385
|
+
if (typeof value.name !== "string") {
|
|
386
|
+
pushError(errors, `${path}.name`, `${path}.name must be a string`);
|
|
387
|
+
} else {
|
|
388
|
+
validateInterpolatedString(value.name, `${path}.name`, errors);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (value.narrative !== void 0) {
|
|
392
|
+
if (typeof value.narrative !== "string") {
|
|
393
|
+
pushError(errors, `${path}.narrative`, `${path}.narrative must be a string`);
|
|
394
|
+
} else {
|
|
395
|
+
validateInterpolatedString(value.narrative, `${path}.narrative`, errors);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return true;
|
|
399
|
+
};
|
|
400
|
+
const validateRawCredentialVariables = (value, path, errors) => {
|
|
401
|
+
if (typeof value === "string") {
|
|
402
|
+
validateInterpolatedString(value, path, errors);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
if (Array.isArray(value)) {
|
|
406
|
+
value.forEach(
|
|
407
|
+
(item, index) => validateRawCredentialVariables(item, `${path}[${index}]`, errors)
|
|
408
|
+
);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
if (isPlainObject(value)) {
|
|
412
|
+
Object.keys(value).forEach((key) => {
|
|
413
|
+
validateRawCredentialVariables(value[key], `${path}.${key}`, errors);
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
const validateSimpleTemplate = (template, errors) => {
|
|
418
|
+
if (typeof template.name !== "string" || template.name.trim().length === 0) {
|
|
419
|
+
pushError(errors, "template.name", "template.name is required");
|
|
420
|
+
} else {
|
|
421
|
+
validateInterpolatedString(template.name, "template.name", errors);
|
|
422
|
+
}
|
|
423
|
+
if (template.description !== void 0) {
|
|
424
|
+
if (typeof template.description !== "string") {
|
|
425
|
+
pushError(errors, "template.description", "template.description must be a string");
|
|
426
|
+
} else {
|
|
427
|
+
validateInterpolatedString(template.description, "template.description", errors);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (template.image !== void 0) {
|
|
431
|
+
validateHttpsOrVariableUrl(template.image, "template.image", errors);
|
|
432
|
+
}
|
|
433
|
+
if (template.issuerName !== void 0) {
|
|
434
|
+
if (typeof template.issuerName !== "string") {
|
|
435
|
+
pushError(errors, "template.issuerName", "template.issuerName must be a string");
|
|
436
|
+
} else {
|
|
437
|
+
validateInterpolatedString(template.issuerName, "template.issuerName", errors);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (template.achievementType !== void 0) {
|
|
441
|
+
if (typeof template.achievementType !== "string") {
|
|
442
|
+
pushError(
|
|
443
|
+
errors,
|
|
444
|
+
"template.achievementType",
|
|
445
|
+
"template.achievementType must be a string"
|
|
446
|
+
);
|
|
447
|
+
} else {
|
|
448
|
+
const message = validateAchievementType(template.achievementType);
|
|
449
|
+
if (message !== void 0) {
|
|
450
|
+
pushError(errors, "template.achievementType", message);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (template.category !== void 0 && !validateWalletCategory(template.category)) {
|
|
455
|
+
const suggestion = typeof template.category === "string" ? getDidYouMean(template.category, WALLET_CATEGORIES) : void 0;
|
|
456
|
+
const suffix = suggestion ? `; did you mean "${suggestion}"?` : "";
|
|
457
|
+
pushError(
|
|
458
|
+
errors,
|
|
459
|
+
"template.category",
|
|
460
|
+
`template.category must be one of ${WALLET_CATEGORIES.join(", ")}${suffix}`
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
if (template.criteria !== void 0) {
|
|
464
|
+
if (!isPlainObject(template.criteria)) {
|
|
465
|
+
pushError(errors, "template.criteria", "template.criteria must be an object");
|
|
466
|
+
} else {
|
|
467
|
+
if (template.criteria.narrative !== void 0) {
|
|
468
|
+
if (typeof template.criteria.narrative !== "string") {
|
|
469
|
+
pushError(
|
|
470
|
+
errors,
|
|
471
|
+
"template.criteria.narrative",
|
|
472
|
+
"template.criteria.narrative must be a string"
|
|
473
|
+
);
|
|
474
|
+
} else {
|
|
475
|
+
validateInterpolatedString(
|
|
476
|
+
template.criteria.narrative,
|
|
477
|
+
"template.criteria.narrative",
|
|
478
|
+
errors
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (template.criteria.url !== void 0) {
|
|
483
|
+
validateHttpsOrVariableUrl(template.criteria.url, "template.criteria.url", errors);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
if (template.alignments !== void 0) {
|
|
488
|
+
if (!Array.isArray(template.alignments)) {
|
|
489
|
+
pushError(errors, "template.alignments", "template.alignments must be an array");
|
|
490
|
+
} else {
|
|
491
|
+
template.alignments.forEach((alignment, index) => {
|
|
492
|
+
validateAlignment(alignment, `template.alignments[${index}]`, errors);
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (template.tags !== void 0) {
|
|
497
|
+
validateStringArray(template.tags, "template.tags", errors);
|
|
498
|
+
}
|
|
499
|
+
if (template.activity !== void 0) {
|
|
500
|
+
if (!isPlainObject(template.activity)) {
|
|
501
|
+
pushError(errors, "template.activity", "template.activity must be an object");
|
|
502
|
+
} else {
|
|
503
|
+
if (template.activity.startDate !== void 0) {
|
|
504
|
+
validateTemplateDate(
|
|
505
|
+
template.activity.startDate,
|
|
506
|
+
"template.activity.startDate",
|
|
507
|
+
errors
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
if (template.activity.endDate !== void 0) {
|
|
511
|
+
validateTemplateDate(
|
|
512
|
+
template.activity.endDate,
|
|
513
|
+
"template.activity.endDate",
|
|
514
|
+
errors
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (template.credits !== void 0) {
|
|
520
|
+
if (!isPlainObject(template.credits)) {
|
|
521
|
+
pushError(errors, "template.credits", "template.credits must be an object");
|
|
522
|
+
} else {
|
|
523
|
+
if (template.credits.earned !== void 0) {
|
|
524
|
+
validateTemplateNumber(template.credits.earned, "template.credits.earned", errors);
|
|
525
|
+
}
|
|
526
|
+
if (template.credits.available !== void 0) {
|
|
527
|
+
validateTemplateNumber(
|
|
528
|
+
template.credits.available,
|
|
529
|
+
"template.credits.available",
|
|
530
|
+
errors
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (template.validUntil !== void 0) {
|
|
536
|
+
validateTemplateDate(template.validUntil, "template.validUntil", errors);
|
|
537
|
+
}
|
|
538
|
+
if (template.evidence !== void 0) {
|
|
539
|
+
if (!Array.isArray(template.evidence)) {
|
|
540
|
+
pushError(errors, "template.evidence", "template.evidence must be an array");
|
|
541
|
+
} else {
|
|
542
|
+
template.evidence.forEach((evidence, index) => {
|
|
543
|
+
validateEvidence(evidence, `template.evidence[${index}]`, errors);
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
validateWalletSkillTags(template.walletSkills, "template.walletSkills", errors);
|
|
548
|
+
};
|
|
549
|
+
const validateRawTemplate = (template, errors) => {
|
|
550
|
+
const invalidKeys = SIMPLE_TEMPLATE_KEYS.filter(
|
|
551
|
+
(key) => Object.prototype.hasOwnProperty.call(template, key)
|
|
552
|
+
);
|
|
553
|
+
invalidKeys.forEach((key) => {
|
|
554
|
+
pushError(
|
|
555
|
+
errors,
|
|
556
|
+
"template.rawCredential",
|
|
557
|
+
`template.rawCredential cannot be combined with template.${key} \u2014 use one or the other`
|
|
558
|
+
);
|
|
559
|
+
});
|
|
560
|
+
if (!isPlainObject(template.rawCredential)) {
|
|
561
|
+
pushError(
|
|
562
|
+
errors,
|
|
563
|
+
"template.rawCredential",
|
|
564
|
+
"template.rawCredential must be a plain object"
|
|
565
|
+
);
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
if (!Array.isArray(template.rawCredential["@context"])) {
|
|
569
|
+
pushError(
|
|
570
|
+
errors,
|
|
571
|
+
"template.rawCredential.@context",
|
|
572
|
+
"template.rawCredential.@context must be an array"
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
if (!Array.isArray(template.rawCredential.type)) {
|
|
576
|
+
pushError(
|
|
577
|
+
errors,
|
|
578
|
+
"template.rawCredential.type",
|
|
579
|
+
"template.rawCredential.type must be an array"
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
if (template.category !== void 0 && !validateWalletCategory(template.category)) {
|
|
583
|
+
const suggestion = typeof template.category === "string" ? getDidYouMean(template.category, WALLET_CATEGORIES) : void 0;
|
|
584
|
+
const suffix = suggestion ? `; did you mean "${suggestion}"?` : "";
|
|
585
|
+
pushError(
|
|
586
|
+
errors,
|
|
587
|
+
"template.category",
|
|
588
|
+
`template.category must be one of ${WALLET_CATEGORIES.join(", ")}${suffix}`
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
validateWalletSkillTags(template.walletSkills, "template.walletSkills", errors);
|
|
592
|
+
validateRawCredentialVariables(template.rawCredential, "template.rawCredential", errors);
|
|
593
|
+
};
|
|
594
|
+
const validateInlineTemplate = (template) => {
|
|
595
|
+
const errors = [];
|
|
596
|
+
if (!isPlainObject(template)) {
|
|
597
|
+
return [{ path: "template", message: "template must be a plain object" }];
|
|
598
|
+
}
|
|
599
|
+
if ("rawCredential" in template && template.rawCredential !== void 0) {
|
|
600
|
+
validateRawTemplate(template, errors);
|
|
601
|
+
return errors;
|
|
602
|
+
}
|
|
603
|
+
validateSimpleTemplate(template, errors);
|
|
604
|
+
return errors;
|
|
605
|
+
};
|
|
606
|
+
const VARIABLE_SENTINEL_PREFIX = "__LC_VAR__";
|
|
607
|
+
const getStringVariablePaths = (value, path, variables) => {
|
|
608
|
+
var _a;
|
|
609
|
+
if (typeof value === "string") {
|
|
610
|
+
TEMPLATE_VARIABLE_REGEX.lastIndex = 0;
|
|
611
|
+
let match = TEMPLATE_VARIABLE_REGEX.exec(value);
|
|
612
|
+
while (match) {
|
|
613
|
+
const variableName = match[1];
|
|
614
|
+
if (!isReservedTemplateVariable(variableName)) {
|
|
615
|
+
variables[variableName] = (_a = variables[variableName]) != null ? _a : [];
|
|
616
|
+
if (!variables[variableName].includes(path)) {
|
|
617
|
+
variables[variableName].push(path);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
match = TEMPLATE_VARIABLE_REGEX.exec(value);
|
|
621
|
+
}
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
if (Array.isArray(value)) {
|
|
625
|
+
value.forEach(
|
|
626
|
+
(item, index) => getStringVariablePaths(item, `${path}[${index}]`, variables)
|
|
627
|
+
);
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
if (isPlainObject(value)) {
|
|
631
|
+
Object.keys(value).forEach((key) => {
|
|
632
|
+
const nextPath = path === "$" ? `$.${key}` : `${path}.${key}`;
|
|
633
|
+
getStringVariablePaths(value[key], nextPath, variables);
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
};
|
|
637
|
+
const findUnquotedVariableSpans = (json) => {
|
|
638
|
+
const spans = [];
|
|
639
|
+
let inString = false;
|
|
640
|
+
for (let index = 0; index < json.length; index += 1) {
|
|
641
|
+
const char = json[index];
|
|
642
|
+
if (inString) {
|
|
643
|
+
if (char === "\\") index += 1;
|
|
644
|
+
else if (char === '"') inString = false;
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
if (char === '"') {
|
|
648
|
+
inString = true;
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
if (char === "{" && json[index + 1] === "{") {
|
|
652
|
+
const match = /^\{\{(\w+)\}\}/.exec(json.slice(index));
|
|
653
|
+
if (match) {
|
|
654
|
+
spans.push({ name: match[1], start: index, end: index + match[0].length });
|
|
655
|
+
index += match[0].length - 1;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return spans;
|
|
660
|
+
};
|
|
661
|
+
const replaceUnquotedVariablesForParsing = (json) => {
|
|
662
|
+
const spans = findUnquotedVariableSpans(json);
|
|
663
|
+
let result = json;
|
|
664
|
+
for (let index = spans.length - 1; index >= 0; index -= 1) {
|
|
665
|
+
const span = spans[index];
|
|
666
|
+
result = `${result.slice(0, span.start)}"${VARIABLE_SENTINEL_PREFIX}${span.name}"${result.slice(span.end)}`;
|
|
667
|
+
}
|
|
668
|
+
return result;
|
|
669
|
+
};
|
|
670
|
+
const extractTemplateVariables = (json) => {
|
|
671
|
+
const variables = /* @__PURE__ */ new Set();
|
|
672
|
+
TEMPLATE_VARIABLE_REGEX.lastIndex = 0;
|
|
673
|
+
let match = TEMPLATE_VARIABLE_REGEX.exec(json);
|
|
674
|
+
while (match) {
|
|
675
|
+
variables.add(match[1]);
|
|
676
|
+
match = TEMPLATE_VARIABLE_REGEX.exec(json);
|
|
677
|
+
}
|
|
678
|
+
return Array.from(variables).sort((a, b) => a.localeCompare(b));
|
|
679
|
+
};
|
|
680
|
+
const buildVariableManifest = (credentialTemplateJson) => {
|
|
681
|
+
const numericVariables = /* @__PURE__ */ new Set();
|
|
682
|
+
const stringPaths = {};
|
|
683
|
+
findUnquotedVariableSpans(credentialTemplateJson).forEach((span) => {
|
|
684
|
+
if (!isReservedTemplateVariable(span.name)) {
|
|
685
|
+
numericVariables.add(span.name);
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
try {
|
|
689
|
+
const parsed = JSON.parse(
|
|
690
|
+
replaceUnquotedVariablesForParsing(credentialTemplateJson)
|
|
691
|
+
);
|
|
692
|
+
getStringVariablePaths(parsed, "$", stringPaths);
|
|
693
|
+
} catch {
|
|
694
|
+
}
|
|
695
|
+
const manifestVariables = {};
|
|
696
|
+
extractTemplateVariables(credentialTemplateJson).forEach((variableName) => {
|
|
697
|
+
var _a;
|
|
698
|
+
if (isReservedTemplateVariable(variableName)) {
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
const paths = [...(_a = stringPaths[variableName]) != null ? _a : []].sort((a, b) => a.localeCompare(b));
|
|
702
|
+
manifestVariables[variableName] = {
|
|
703
|
+
type: numericVariables.has(variableName) ? "number" : "string",
|
|
704
|
+
paths
|
|
705
|
+
};
|
|
706
|
+
});
|
|
707
|
+
return { variables: manifestVariables };
|
|
708
|
+
};
|
|
709
|
+
const validateTemplateData = (manifest, templateData) => {
|
|
710
|
+
const errors = [];
|
|
711
|
+
const data = templateData != null ? templateData : {};
|
|
712
|
+
const manifestKeys = Object.keys(manifest.variables);
|
|
713
|
+
manifestKeys.forEach((variableName) => {
|
|
714
|
+
if (!Object.prototype.hasOwnProperty.call(data, variableName) || data[variableName] === void 0) {
|
|
715
|
+
errors.push({
|
|
716
|
+
path: `templateData.${variableName}`,
|
|
717
|
+
message: `templateData.${variableName} is required`
|
|
718
|
+
});
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
const expectedType = manifest.variables[variableName].type;
|
|
722
|
+
const value = data[variableName];
|
|
723
|
+
if (expectedType === "number") {
|
|
724
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
725
|
+
errors.push({
|
|
726
|
+
path: `templateData.${variableName}`,
|
|
727
|
+
message: `templateData.${variableName} must be a finite number`
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
if (!["string", "number", "boolean"].includes(typeof value)) {
|
|
733
|
+
errors.push({
|
|
734
|
+
path: `templateData.${variableName}`,
|
|
735
|
+
message: `templateData.${variableName} must be a string, number, or boolean`
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
Object.keys(data).forEach((key) => {
|
|
740
|
+
if (key === "evidence") {
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
if (RESERVED_TEMPLATE_VARIABLES.includes(key)) {
|
|
744
|
+
errors.push({
|
|
745
|
+
path: `templateData.${key}`,
|
|
746
|
+
message: `templateData.${key} is injected automatically by LearnCard and cannot be supplied`
|
|
747
|
+
});
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
if (!Object.prototype.hasOwnProperty.call(manifest.variables, key)) {
|
|
751
|
+
const suggestion = getDidYouMean(key, manifestKeys);
|
|
752
|
+
const suffix = suggestion ? `; did you mean "${suggestion}"?` : "";
|
|
753
|
+
errors.push({
|
|
754
|
+
path: `templateData.${key}`,
|
|
755
|
+
message: `templateData.${key} is unused${suffix}`
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
return errors;
|
|
760
|
+
};
|
|
761
|
+
const NUMERIC_SENTINEL_PREFIX = "__LC_NUM__";
|
|
762
|
+
const toNumericSentinel = (value) => {
|
|
763
|
+
const variableName = getBareVariableName(value);
|
|
764
|
+
return variableName !== void 0 ? `${NUMERIC_SENTINEL_PREFIX}{{${variableName}}}__` : value;
|
|
765
|
+
};
|
|
766
|
+
const finalizeCredentialTemplateJson = (value) => {
|
|
767
|
+
const canonical = canonicalJsonString(value);
|
|
768
|
+
return canonical.replace(/"__LC_NUM__(\{\{\w+\}\})__"/g, "$1");
|
|
769
|
+
};
|
|
770
|
+
const compileSimpleTemplate = (template) => {
|
|
771
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i;
|
|
772
|
+
const credential = stripEmpty({
|
|
773
|
+
"@context": [VC2_CONTEXT, OBV3_CONTEXT],
|
|
774
|
+
type: ["VerifiableCredential", "OpenBadgeCredential"],
|
|
775
|
+
name: template.name,
|
|
776
|
+
description: template.description,
|
|
777
|
+
image: template.image,
|
|
778
|
+
issuer: stripEmpty({
|
|
779
|
+
id: "{{issuer_did}}",
|
|
780
|
+
type: ["Profile"],
|
|
781
|
+
name: template.issuerName
|
|
782
|
+
}),
|
|
783
|
+
validFrom: "{{issue_date}}",
|
|
784
|
+
validUntil: template.validUntil,
|
|
785
|
+
credentialSubject: stripEmpty({
|
|
786
|
+
type: ["AchievementSubject"],
|
|
787
|
+
id: "{{recipient_did}}",
|
|
788
|
+
achievement: stripEmpty({
|
|
789
|
+
type: ["Achievement"],
|
|
790
|
+
name: template.name,
|
|
791
|
+
description: template.description,
|
|
792
|
+
achievementType: (_a = template.achievementType) != null ? _a : "Badge",
|
|
793
|
+
image: template.image,
|
|
794
|
+
criteria: stripEmpty({
|
|
795
|
+
narrative: (_b = template.criteria) == null ? void 0 : _b.narrative,
|
|
796
|
+
id: (_c = template.criteria) == null ? void 0 : _c.url
|
|
797
|
+
}),
|
|
798
|
+
alignment: (_d = template.alignments) == null ? void 0 : _d.map(
|
|
799
|
+
(alignment) => stripEmpty({
|
|
800
|
+
type: ["Alignment"],
|
|
801
|
+
targetName: alignment.name,
|
|
802
|
+
targetUrl: alignment.url,
|
|
803
|
+
targetFramework: alignment.framework,
|
|
804
|
+
targetCode: alignment.code
|
|
805
|
+
})
|
|
806
|
+
),
|
|
807
|
+
tag: template.tags
|
|
808
|
+
}),
|
|
809
|
+
activityStartDate: (_e = template.activity) == null ? void 0 : _e.startDate,
|
|
810
|
+
activityEndDate: (_f = template.activity) == null ? void 0 : _f.endDate,
|
|
811
|
+
creditsEarned: toNumericSentinel((_g = template.credits) == null ? void 0 : _g.earned),
|
|
812
|
+
creditsAvailable: toNumericSentinel((_h = template.credits) == null ? void 0 : _h.available)
|
|
813
|
+
}),
|
|
814
|
+
evidence: (_i = template.evidence) == null ? void 0 : _i.map(
|
|
815
|
+
(evidence) => stripEmpty({
|
|
816
|
+
type: ["Evidence"],
|
|
817
|
+
id: evidence.id,
|
|
818
|
+
name: evidence.name,
|
|
819
|
+
narrative: evidence.narrative
|
|
820
|
+
})
|
|
821
|
+
)
|
|
822
|
+
});
|
|
823
|
+
return finalizeCredentialTemplateJson(credential);
|
|
824
|
+
};
|
|
825
|
+
const forceCredentialSubjectId = (credentialSubject) => {
|
|
826
|
+
if (Array.isArray(credentialSubject)) {
|
|
827
|
+
credentialSubject.forEach((subject) => {
|
|
828
|
+
if (isPlainObject(subject)) {
|
|
829
|
+
subject.id = "{{recipient_did}}";
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
if (isPlainObject(credentialSubject)) {
|
|
835
|
+
credentialSubject.id = "{{recipient_did}}";
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
const compileRawTemplate = (template) => {
|
|
839
|
+
const credential = deepClone(template.rawCredential);
|
|
840
|
+
if (!isPlainObject(credential)) {
|
|
841
|
+
throw new Error("template.rawCredential must be a plain object");
|
|
842
|
+
}
|
|
843
|
+
if (isPlainObject(credential.issuer)) {
|
|
844
|
+
credential.issuer.id = "{{issuer_did}}";
|
|
845
|
+
} else {
|
|
846
|
+
credential.issuer = { id: "{{issuer_did}}", type: ["Profile"] };
|
|
847
|
+
}
|
|
848
|
+
credential.validFrom = "{{issue_date}}";
|
|
849
|
+
forceCredentialSubjectId(credential.credentialSubject);
|
|
850
|
+
return finalizeCredentialTemplateJson(credential);
|
|
851
|
+
};
|
|
852
|
+
const compileInlineTemplate = (template) => {
|
|
853
|
+
var _a;
|
|
854
|
+
const errors = validateInlineTemplate(template);
|
|
855
|
+
if (errors.length > 0) {
|
|
856
|
+
throw new Error(errors.map((error) => `${error.path}: ${error.message}`).join("\n"));
|
|
857
|
+
}
|
|
858
|
+
const credentialTemplateJson = "rawCredential" in template && template.rawCredential !== void 0 ? compileRawTemplate(template) : compileSimpleTemplate(template);
|
|
859
|
+
return {
|
|
860
|
+
credentialTemplateJson,
|
|
861
|
+
variableManifest: buildVariableManifest(credentialTemplateJson),
|
|
862
|
+
category: (_a = template.category) != null ? _a : "Achievement",
|
|
863
|
+
walletSkills: template.walletSkills
|
|
864
|
+
};
|
|
865
|
+
};
|
|
866
|
+
const normalizeCategoryArray = (values, path) => {
|
|
867
|
+
if (values === void 0) {
|
|
868
|
+
return [];
|
|
869
|
+
}
|
|
870
|
+
if (!Array.isArray(values)) {
|
|
871
|
+
throw new Error(`${path} must be an array`);
|
|
872
|
+
}
|
|
873
|
+
const normalized = values.map((value, index) => {
|
|
874
|
+
if (!validateWalletCategory(value)) {
|
|
875
|
+
const suggestion = typeof value === "string" ? getDidYouMean(value, WALLET_CATEGORIES) : void 0;
|
|
876
|
+
const suffix = suggestion ? `; did you mean "${suggestion}"?` : "";
|
|
877
|
+
throw new Error(
|
|
878
|
+
`${path}[${index}] has unknown wallet category "${String(value)}"${suffix}`
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
return value;
|
|
882
|
+
});
|
|
883
|
+
return sortUnique(normalized);
|
|
884
|
+
};
|
|
885
|
+
const normalizePersonalFieldArray = (values, path) => {
|
|
886
|
+
if (values === void 0) {
|
|
887
|
+
return [];
|
|
888
|
+
}
|
|
889
|
+
if (!Array.isArray(values)) {
|
|
890
|
+
throw new Error(`${path} must be an array`);
|
|
891
|
+
}
|
|
892
|
+
const normalized = values.map((value, index) => {
|
|
893
|
+
if (!validatePersonalField(value)) {
|
|
894
|
+
const suggestion = typeof value === "string" ? getDidYouMean(value, PERSONAL_FIELDS) : void 0;
|
|
895
|
+
const suffix = suggestion ? `; did you mean "${suggestion}"?` : "";
|
|
896
|
+
throw new Error(
|
|
897
|
+
`${path}[${index}] has unknown personal field "${String(value)}"${suffix}`
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
return value;
|
|
901
|
+
});
|
|
902
|
+
return sortUnique(normalized);
|
|
903
|
+
};
|
|
904
|
+
const normalizeConsentRequest = (req) => {
|
|
905
|
+
var _a, _b, _c;
|
|
906
|
+
return {
|
|
907
|
+
read: {
|
|
908
|
+
credentialCategories: normalizeCategoryArray(
|
|
909
|
+
(_a = req.read) == null ? void 0 : _a.credentialCategories,
|
|
910
|
+
"consent.read.credentialCategories"
|
|
911
|
+
),
|
|
912
|
+
personalFields: normalizePersonalFieldArray(
|
|
913
|
+
(_b = req.read) == null ? void 0 : _b.personalFields,
|
|
914
|
+
"consent.read.personalFields"
|
|
915
|
+
)
|
|
916
|
+
},
|
|
917
|
+
write: {
|
|
918
|
+
credentialCategories: normalizeCategoryArray(
|
|
919
|
+
(_c = req.write) == null ? void 0 : _c.credentialCategories,
|
|
920
|
+
"consent.write.credentialCategories"
|
|
921
|
+
)
|
|
922
|
+
}
|
|
923
|
+
};
|
|
924
|
+
};
|
|
925
|
+
const canonicalConsentScopeString = (scopes) => {
|
|
926
|
+
return canonicalJsonString(scopes);
|
|
927
|
+
};
|
|
928
|
+
const getTextEncoder = () => {
|
|
929
|
+
if (typeof TextEncoder !== "undefined") return new TextEncoder();
|
|
930
|
+
return void 0;
|
|
931
|
+
};
|
|
932
|
+
const getTextDecoder = () => {
|
|
933
|
+
if (typeof TextDecoder !== "undefined") return new TextDecoder();
|
|
934
|
+
return void 0;
|
|
935
|
+
};
|
|
936
|
+
const getBuffer = () => {
|
|
937
|
+
const candidate = globalThis.Buffer;
|
|
938
|
+
return candidate;
|
|
939
|
+
};
|
|
940
|
+
const toBase64 = (value) => {
|
|
941
|
+
const buffer = getBuffer();
|
|
942
|
+
if (buffer) return buffer.from(value, "utf8").toString("base64");
|
|
943
|
+
const encoder = getTextEncoder();
|
|
944
|
+
if (encoder && typeof btoa === "function") {
|
|
945
|
+
const bytes = encoder.encode(value);
|
|
946
|
+
let binary = "";
|
|
947
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
948
|
+
return btoa(binary);
|
|
949
|
+
}
|
|
950
|
+
if (typeof btoa === "function") {
|
|
951
|
+
return btoa(unescape(encodeURIComponent(value)));
|
|
952
|
+
}
|
|
953
|
+
throw new Error("No base64 encoder available in this environment");
|
|
954
|
+
};
|
|
955
|
+
const fromBase64 = (value) => {
|
|
956
|
+
const buffer = getBuffer();
|
|
957
|
+
if (buffer) return buffer.from(value, "base64").toString("utf8");
|
|
958
|
+
if (typeof atob === "function") {
|
|
959
|
+
const binary = atob(value);
|
|
960
|
+
const decoder = getTextDecoder();
|
|
961
|
+
if (decoder) {
|
|
962
|
+
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
963
|
+
return decoder.decode(bytes);
|
|
964
|
+
}
|
|
965
|
+
return decodeURIComponent(
|
|
966
|
+
binary.split("").map((char) => `%${char.charCodeAt(0).toString(16).padStart(2, "0")}`).join("")
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
throw new Error("No base64 decoder available in this environment");
|
|
970
|
+
};
|
|
971
|
+
const toBase64Url = (value) => toBase64(value).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
972
|
+
const fromBase64Url = (value) => {
|
|
973
|
+
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
974
|
+
const paddingLength = (4 - normalized.length % 4) % 4;
|
|
975
|
+
return fromBase64(`${normalized}${"=".repeat(paddingLength)}`);
|
|
976
|
+
};
|
|
977
|
+
const encodeManifestForUrl = (manifest) => toBase64Url(JSON.stringify(manifest));
|
|
978
|
+
const decodeManifestFromUrl = (value) => JSON.parse(fromBase64Url(value));
|
|
979
|
+
const needsConsentSetup = (manifest) => manifest.permissions.includes("request_consent") && manifest.consentRequests.length === 0 && !manifest.usedConsentContract;
|
|
980
|
+
const renderCompiledTemplate = (credentialTemplateJson, templateData) => {
|
|
981
|
+
const renderedJson = credentialTemplateJson.replace(
|
|
982
|
+
/\{\{(\w+)\}\}/g,
|
|
983
|
+
(match, variableName) => {
|
|
984
|
+
if (!Object.prototype.hasOwnProperty.call(templateData, variableName)) {
|
|
985
|
+
return ["issue_date", "issuer_did", "recipient_did"].includes(variableName) ? match : "";
|
|
986
|
+
}
|
|
987
|
+
const value = templateData[variableName];
|
|
988
|
+
if (value === null || value === void 0) {
|
|
989
|
+
return "";
|
|
990
|
+
}
|
|
991
|
+
if (typeof value === "string") {
|
|
992
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t").replace(/\f/g, "\\f").split(String.fromCharCode(8)).join("\\b");
|
|
993
|
+
}
|
|
994
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
995
|
+
return String(value);
|
|
996
|
+
}
|
|
997
|
+
return "";
|
|
998
|
+
}
|
|
999
|
+
);
|
|
1000
|
+
try {
|
|
1001
|
+
return JSON.parse(renderedJson);
|
|
1002
|
+
} catch (error) {
|
|
1003
|
+
const message = error instanceof Error ? error.message : "Unknown JSON parse error";
|
|
1004
|
+
throw new Error(`Failed to render compiled template into valid JSON: ${message}`, {
|
|
1005
|
+
cause: error
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
};
|
|
1009
|
+
|
|
5
1010
|
var __defProp$2 = Object.defineProperty;
|
|
6
1011
|
var __defNormalProp$2 = (obj, key, value) => key in obj ? __defProp$2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
7
1012
|
var __publicField$2 = (obj, key, value) => __defNormalProp$2(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
@@ -40,13 +1045,333 @@ var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key,
|
|
|
40
1045
|
var __publicField$1 = (obj, key, value) => __defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
41
1046
|
const DEFAULT_DID = "did:web:mock.learncard.app:user";
|
|
42
1047
|
const DEFAULT_NAMESPACE = "lc-mock";
|
|
1048
|
+
const DEFAULT_PUBLISH_ORIGIN$1 = "https://learncard.app";
|
|
43
1049
|
const MOCK_PREFIX = "[LearnCard SDK \xB7 MOCK]";
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
1050
|
+
const PUBLISH_DISMISS_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
1051
|
+
const ICON_PATHS = {
|
|
1052
|
+
user: '<circle cx="12" cy="8" r="4"/><path d="M4 21c0-4 3.6-6.5 8-6.5s8 2.5 8 6.5"/>',
|
|
1053
|
+
award: '<circle cx="12" cy="9" r="6"/><path d="M8.6 14 7 22l5-3 5 3-1.6-8"/>',
|
|
1054
|
+
shield: '<path d="M12 3 5 6v6c0 4.4 3 7.6 7 9 4-1.4 7-4.6 7-9V6z"/><path d="m9 12 2 2 4-4"/>',
|
|
1055
|
+
open: '<path d="M7 17 17 7"/><path d="M8 7h9v9"/>',
|
|
1056
|
+
search: '<circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/>',
|
|
1057
|
+
send: '<path d="M21 3 10 14"/><path d="M21 3 14.5 21l-4.5-7-7-4.5z"/>',
|
|
1058
|
+
sparkles: '<path d="M12 3.5 13.9 9l5.6 2-5.6 2L12 18.5 10.1 13l-5.6-2 5.6-2z"/><path d="M19 3v4M17 5h4"/>',
|
|
1059
|
+
sync: '<path d="M20 12a8 8 0 0 1-14.3 4.9M4 12a8 8 0 0 1 14.3-4.9"/><path d="M18.5 3v4h-4M5.5 21v-4h4"/>',
|
|
1060
|
+
bell: '<path d="M6 9a6 6 0 1 1 12 0c0 6.5 2.5 8.5 2.5 8.5h-17S6 15.5 6 9"/><path d="M10.3 20.5a2 2 0 0 0 3.4 0"/>',
|
|
1061
|
+
hash: '<path d="M4 9h16M4 15h16M10 3 8 21M16 3l-2 18"/>',
|
|
1062
|
+
users: '<circle cx="9" cy="8" r="3.5"/><path d="M2.5 20c0-3.5 2.9-6 6.5-6s6.5 2.5 6.5 6"/><path d="M16 4.5a3.5 3.5 0 0 1 0 7M21.5 20c0-2.8-1.6-4.9-4-5.7"/>',
|
|
1063
|
+
publish: '<path d="M12 19V5"/><path d="m5 12 7-7 7 7"/>',
|
|
1064
|
+
mark: '<rect x="3" y="5.5" width="18" height="13" rx="3"/><path d="M7 10h6M7 14h4"/>',
|
|
1065
|
+
copy: '<rect x="9" y="9" width="11" height="11" rx="2.5"/><path d="M5 15V6.5A2.5 2.5 0 0 1 7.5 4H15"/>',
|
|
1066
|
+
check: '<path d="m5 12.5 4.5 4.5L19 7.5"/>',
|
|
1067
|
+
close: '<path d="M17 7 7 17M7 7l10 10"/>',
|
|
1068
|
+
chevronDown: '<path d="m6 9 6 6 6-6"/>'
|
|
1069
|
+
};
|
|
1070
|
+
const MAX_VISIBLE_TOASTS = 3;
|
|
1071
|
+
const PERMISSION_FEATURES = {
|
|
1072
|
+
request_identity: { icon: "user", label: "Sign in with LearnCard" },
|
|
1073
|
+
send_credential: { icon: "award", label: "Award credentials" },
|
|
1074
|
+
template_issuance: { icon: "send", label: "Send from your templates" },
|
|
1075
|
+
request_consent: { icon: "shield", label: "Ask for permission" },
|
|
1076
|
+
credential_search: { icon: "search", label: "Ask learners to share credentials" },
|
|
1077
|
+
credential_by_id: { icon: "search", label: "Request a specific credential" },
|
|
1078
|
+
launch_feature: { icon: "open", label: "Open LearnCard screens" }
|
|
1079
|
+
};
|
|
1080
|
+
const MOCK_STYLES = `
|
|
1081
|
+
@keyframes lc-mock-in { from { opacity: 0; transform: translateY(14px) scale(0.96); filter: blur(6px); } to { opacity: 1; transform: none; filter: none; } }
|
|
1082
|
+
@keyframes lc-mock-in-top { from { opacity: 0; transform: translateY(-14px) scale(0.96); filter: blur(6px); } to { opacity: 1; transform: none; filter: none; } }
|
|
1083
|
+
@keyframes lc-mock-out { to { opacity: 0; transform: scale(0.94); filter: blur(4px); } }
|
|
1084
|
+
@keyframes lc-mock-pop { from { opacity: 0; transform: translateY(10px) scale(0.94); } to { opacity: 1; transform: none; } }
|
|
1085
|
+
@keyframes lc-mock-bump { 40% { transform: scale(1.18); } }
|
|
1086
|
+
@keyframes lc-mock-pulse { 0% { box-shadow: 0 0 0 0 rgba(16,185,129,0.5); } 70%, 100% { box-shadow: 0 0 0 6px rgba(16,185,129,0); } }
|
|
1087
|
+
|
|
1088
|
+
.lc-mock-stack, .lc-mock-hud {
|
|
1089
|
+
--lc-glass: rgba(255,255,255,0.72);
|
|
1090
|
+
--lc-glass-solid: rgba(251,251,252,0.97);
|
|
1091
|
+
--lc-group: rgba(255,255,255,0.66);
|
|
1092
|
+
--lc-edge: rgba(255,255,255,0.7);
|
|
1093
|
+
--lc-hairline: rgba(24,34,78,0.08);
|
|
1094
|
+
--lc-fill: rgba(24,34,78,0.06);
|
|
1095
|
+
--lc-fill-hover: rgba(24,34,78,0.1);
|
|
1096
|
+
--lc-ink: #18224E;
|
|
1097
|
+
--lc-ink-2: #52597A;
|
|
1098
|
+
--lc-ink-3: #8B91A7;
|
|
1099
|
+
--lc-solid: #18224E;
|
|
1100
|
+
--lc-on-solid: #FFFFFF;
|
|
1101
|
+
--lc-shadow: 0 0 0 0.5px rgba(24,34,78,0.1), 0 2px 8px rgba(24,34,78,0.06), 0 24px 56px -16px rgba(24,34,78,0.32);
|
|
1102
|
+
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
1103
|
+
font-size: 13px; line-height: 1.4; letter-spacing: -0.006em; text-align: left;
|
|
1104
|
+
color: var(--lc-ink); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
1105
|
+
position: fixed; z-index: 2147483647;
|
|
1106
|
+
}
|
|
1107
|
+
@media (prefers-color-scheme: dark) {
|
|
1108
|
+
.lc-mock-stack, .lc-mock-hud {
|
|
1109
|
+
--lc-glass: rgba(30,37,68,0.62);
|
|
1110
|
+
--lc-glass-solid: rgba(30,37,68,0.97);
|
|
1111
|
+
--lc-group: rgba(255,255,255,0.06);
|
|
1112
|
+
--lc-edge: rgba(255,255,255,0.14);
|
|
1113
|
+
--lc-hairline: rgba(255,255,255,0.08);
|
|
1114
|
+
--lc-fill: rgba(255,255,255,0.1);
|
|
1115
|
+
--lc-fill-hover: rgba(255,255,255,0.16);
|
|
1116
|
+
--lc-ink: #FBFBFC;
|
|
1117
|
+
--lc-ink-2: #C5C8D3;
|
|
1118
|
+
--lc-ink-3: #A8ACBD;
|
|
1119
|
+
--lc-solid: #FBFBFC;
|
|
1120
|
+
--lc-on-solid: #18224E;
|
|
1121
|
+
--lc-shadow: 0 0 0 0.5px rgba(0,0,0,0.4), 0 2px 8px rgba(0,0,0,0.2), 0 24px 56px -16px rgba(0,0,0,0.6);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
.lc-mock-stack *, .lc-mock-hud * { box-sizing: border-box; }
|
|
1125
|
+
.lc-mock-stack svg, .lc-mock-hud svg { display: block; flex: none; }
|
|
1126
|
+
.lc-mock-stack button, .lc-mock-hud button {
|
|
1127
|
+
font: inherit; color: inherit; margin: 0; text-transform: none; letter-spacing: inherit;
|
|
1128
|
+
-webkit-appearance: none; appearance: none; -webkit-tap-highlight-color: transparent;
|
|
1129
|
+
}
|
|
1130
|
+
.lc-mock-stack a, .lc-mock-hud a { -webkit-tap-highlight-color: transparent; }
|
|
1131
|
+
.lc-mock-stack :focus-visible, .lc-mock-hud :focus-visible { outline: 2px solid #10B981; outline-offset: 2px; }
|
|
1132
|
+
|
|
1133
|
+
.lc-mock-glass {
|
|
1134
|
+
background: var(--lc-glass);
|
|
1135
|
+
-webkit-backdrop-filter: saturate(180%) blur(28px); backdrop-filter: saturate(180%) blur(28px);
|
|
1136
|
+
border: 0.5px solid var(--lc-edge);
|
|
1137
|
+
box-shadow: inset 0 1px 0 rgba(255,255,255,0.35), var(--lc-shadow);
|
|
1138
|
+
}
|
|
1139
|
+
@supports not ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
|
|
1140
|
+
.lc-mock-glass { background: var(--lc-glass-solid); }
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
.lc-mock-dot {
|
|
1144
|
+
width: 7px; height: 7px; border-radius: 999px; background: #10B981; flex: none;
|
|
1145
|
+
animation: lc-mock-pulse 2.4s ease-out infinite;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
/* Notices */
|
|
1149
|
+
.lc-mock-stack {
|
|
1150
|
+
bottom: max(20px, calc(env(safe-area-inset-bottom, 0px) + 12px));
|
|
1151
|
+
right: max(20px, calc(env(safe-area-inset-right, 0px) + 12px));
|
|
1152
|
+
width: min(360px, calc(100vw - 40px));
|
|
1153
|
+
display: flex; flex-direction: column; gap: 10px; pointer-events: none;
|
|
1154
|
+
}
|
|
1155
|
+
.lc-mock-toast {
|
|
1156
|
+
position: relative; pointer-events: auto; display: flex; align-items: flex-start; gap: 12px;
|
|
1157
|
+
width: 100%; padding: 12px 14px 13px 12px; border-radius: 22px;
|
|
1158
|
+
animation: lc-mock-in 460ms cubic-bezier(0.22, 1, 0.36, 1) backwards;
|
|
1159
|
+
transition: transform 240ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
1160
|
+
}
|
|
1161
|
+
.lc-mock-toast:hover { transform: translateY(-2px); }
|
|
1162
|
+
.lc-mock-toast.lc-mock-out { animation: lc-mock-out 200ms cubic-bezier(0.4, 0, 1, 1) forwards; pointer-events: none; }
|
|
1163
|
+
.lc-mock-toast--hidden { display: none; }
|
|
1164
|
+
.lc-mock-toast--publish { order: -1; padding: 14px 14px 14px 12px; }
|
|
1165
|
+
.lc-mock-toast--dismissible .lc-mock-content { padding-right: 22px; }
|
|
1166
|
+
|
|
1167
|
+
.lc-mock-tile {
|
|
1168
|
+
width: 34px; height: 34px; border-radius: 10px; display: flex; align-items: center; justify-content: center; flex: none;
|
|
1169
|
+
color: #FFFFFF; background: linear-gradient(180deg, #353E64 0%, #18224E 100%);
|
|
1170
|
+
box-shadow: inset 0 0 0 0.5px rgba(255,255,255,0.18), 0 1px 2px rgba(24,34,78,0.24);
|
|
1171
|
+
}
|
|
1172
|
+
.lc-mock-tile svg { width: 18px; height: 18px; }
|
|
1173
|
+
.lc-mock-toast--positive .lc-mock-tile { background: linear-gradient(180deg, #34D399 0%, #059669 100%); }
|
|
1174
|
+
.lc-mock-toast--warning .lc-mock-tile { background: linear-gradient(180deg, #FBBF24 0%, #D97706 100%); }
|
|
1175
|
+
.lc-mock-hud-row-detail.is-setup { color: #B45309; font-weight: 500; }
|
|
1176
|
+
.lc-mock-hud-setup-dot { width: 8px; height: 8px; margin: 0 4px; border-radius: 999px; background: #F59E0B; flex: none; }
|
|
1177
|
+
|
|
1178
|
+
.lc-mock-content { flex: 1; min-width: 0; }
|
|
1179
|
+
.lc-mock-meta { display: flex; align-items: center; gap: 6px; margin-bottom: 2px; font-size: 11.5px; font-weight: 600; color: var(--lc-ink-3); }
|
|
1180
|
+
.lc-mock-chip { padding: 1px 7px; border-radius: 999px; background: var(--lc-fill); color: var(--lc-ink-2); font-size: 10.5px; font-weight: 600; }
|
|
1181
|
+
.lc-mock-count {
|
|
1182
|
+
margin-left: auto; min-width: 22px; padding: 1px 7px; border-radius: 999px; text-align: center;
|
|
1183
|
+
background: var(--lc-solid); color: var(--lc-on-solid); font-size: 11px; font-weight: 600; font-variant-numeric: tabular-nums;
|
|
1184
|
+
}
|
|
1185
|
+
.lc-mock-bump { animation: lc-mock-bump 260ms ease-out; }
|
|
1186
|
+
.lc-mock-title { font-size: 14.5px; font-weight: 600; letter-spacing: -0.012em; margin: 1px 0 2px; }
|
|
1187
|
+
.lc-mock-body { font-size: 13.5px; line-height: 1.42; color: var(--lc-ink); overflow-wrap: anywhere; white-space: pre-line; }
|
|
1188
|
+
.lc-mock-body strong { font-weight: 600; }
|
|
1189
|
+
.lc-mock-toast--publish .lc-mock-body { color: var(--lc-ink-2); }
|
|
1190
|
+
|
|
1191
|
+
.lc-mock-action {
|
|
1192
|
+
display: inline-flex; align-items: center; gap: 6px; margin-top: 11px; padding: 9px 15px; border-radius: 999px;
|
|
1193
|
+
background: var(--lc-solid); color: var(--lc-on-solid) !important; text-decoration: none !important;
|
|
1194
|
+
font-size: 13px; font-weight: 600; line-height: 1;
|
|
1195
|
+
transition: transform 160ms ease, opacity 160ms ease;
|
|
1196
|
+
}
|
|
1197
|
+
.lc-mock-action svg { width: 14px; height: 14px; }
|
|
1198
|
+
.lc-mock-action:hover { opacity: 0.9; }
|
|
1199
|
+
.lc-mock-action:active { transform: scale(0.97); }
|
|
1200
|
+
|
|
1201
|
+
.lc-mock-close {
|
|
1202
|
+
position: absolute; top: 10px; right: 10px; width: 24px; height: 24px; padding: 0; border: 0; border-radius: 999px;
|
|
1203
|
+
display: flex; align-items: center; justify-content: center; cursor: pointer;
|
|
1204
|
+
background: var(--lc-fill); color: var(--lc-ink-2); transition: background 160ms ease, color 160ms ease;
|
|
1205
|
+
}
|
|
1206
|
+
.lc-mock-close svg { width: 13px; height: 13px; }
|
|
1207
|
+
.lc-mock-close:hover { background: var(--lc-fill-hover); color: var(--lc-ink); }
|
|
1208
|
+
|
|
1209
|
+
/* Practice panel */
|
|
1210
|
+
.lc-mock-hud {
|
|
1211
|
+
left: max(20px, calc(env(safe-area-inset-left, 0px) + 12px));
|
|
1212
|
+
bottom: max(20px, calc(env(safe-area-inset-bottom, 0px) + 12px));
|
|
1213
|
+
}
|
|
1214
|
+
.lc-mock-hud.lc-mock-animate > * { animation: lc-mock-pop 380ms cubic-bezier(0.22, 1, 0.36, 1) backwards; transform-origin: bottom left; }
|
|
1215
|
+
|
|
1216
|
+
.lc-mock-hud-pill {
|
|
1217
|
+
display: inline-flex; align-items: center; gap: 8px; height: 40px; padding: 0 8px 0 6px; border-radius: 999px;
|
|
1218
|
+
cursor: pointer; font-size: 13px; font-weight: 600; color: var(--lc-ink);
|
|
1219
|
+
transition: transform 240ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
1220
|
+
}
|
|
1221
|
+
.lc-mock-hud-pill:hover { transform: translateY(-2px); }
|
|
1222
|
+
.lc-mock-hud-pill:active { transform: scale(0.97); }
|
|
1223
|
+
.lc-mock-hud-pill .lc-mock-dot { margin-left: 2px; }
|
|
1224
|
+
.lc-mock-hud-mark {
|
|
1225
|
+
width: 28px; height: 28px; border-radius: 999px; display: flex; align-items: center; justify-content: center; flex: none;
|
|
1226
|
+
color: #FFFFFF; background: linear-gradient(180deg, #353E64 0%, #18224E 100%);
|
|
1227
|
+
box-shadow: inset 0 0 0 0.5px rgba(255,255,255,0.18);
|
|
1228
|
+
}
|
|
1229
|
+
.lc-mock-hud-mark svg { width: 15px; height: 15px; }
|
|
1230
|
+
.lc-mock-hud-mark--lg { width: 36px; height: 36px; border-radius: 11px; }
|
|
1231
|
+
.lc-mock-hud-mark--lg svg { width: 19px; height: 19px; }
|
|
1232
|
+
.lc-mock-hud-badge {
|
|
1233
|
+
min-width: 24px; height: 24px; padding: 0 7px; border-radius: 999px; display: inline-flex; align-items: center; justify-content: center;
|
|
1234
|
+
background: var(--lc-fill); color: var(--lc-ink-2); font-size: 12px; font-weight: 600; font-variant-numeric: tabular-nums;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
.lc-mock-hud-card {
|
|
1238
|
+
width: min(328px, calc(100vw - 40px)); max-height: calc(100vh - 40px); overflow: auto;
|
|
1239
|
+
border-radius: 26px; padding: 8px 8px 10px; overscroll-behavior: contain;
|
|
1240
|
+
}
|
|
1241
|
+
.lc-mock-hud-header { display: flex; align-items: center; gap: 11px; padding: 6px 4px 12px 6px; }
|
|
1242
|
+
.lc-mock-hud-heading { flex: 1; min-width: 0; }
|
|
1243
|
+
.lc-mock-hud-title { display: flex; align-items: center; gap: 7px; font-size: 15px; font-weight: 600; letter-spacing: -0.014em; }
|
|
1244
|
+
.lc-mock-hud-subtitle { font-size: 12px; color: var(--lc-ink-3); margin-top: 1px; }
|
|
1245
|
+
.lc-mock-hud-minimize {
|
|
1246
|
+
width: 30px; height: 30px; padding: 0; border: 0; border-radius: 999px; flex: none; cursor: pointer;
|
|
1247
|
+
display: flex; align-items: center; justify-content: center;
|
|
1248
|
+
background: var(--lc-fill); color: var(--lc-ink-2); transition: background 160ms ease, color 160ms ease;
|
|
1249
|
+
}
|
|
1250
|
+
.lc-mock-hud-minimize svg { width: 16px; height: 16px; }
|
|
1251
|
+
.lc-mock-hud-minimize:hover { background: var(--lc-fill-hover); color: var(--lc-ink); }
|
|
1252
|
+
|
|
1253
|
+
.lc-mock-hud-group { background: var(--lc-group); border: 0.5px solid var(--lc-hairline); border-radius: 18px; }
|
|
1254
|
+
.lc-mock-hud-app { display: flex; align-items: center; gap: 11px; padding: 10px 12px; }
|
|
1255
|
+
.lc-mock-hud-avatar {
|
|
1256
|
+
width: 36px; height: 36px; border-radius: 10px; flex: none; overflow: hidden;
|
|
1257
|
+
display: flex; align-items: center; justify-content: center;
|
|
1258
|
+
background: var(--lc-fill); color: var(--lc-ink-2); font-size: 15px; font-weight: 600;
|
|
1259
|
+
}
|
|
1260
|
+
.lc-mock-hud-avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
1261
|
+
.lc-mock-hud-app-name { font-size: 14px; font-weight: 600; letter-spacing: -0.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
1262
|
+
|
|
1263
|
+
.lc-mock-hud-section {
|
|
1264
|
+
padding: 14px 12px 6px; font-size: 11px; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; color: var(--lc-ink-3);
|
|
1265
|
+
}
|
|
1266
|
+
.lc-mock-hud-list { list-style: none; margin: 0; padding: 0; max-height: min(40vh, 320px); overflow: auto; }
|
|
1267
|
+
.lc-mock-hud-row { position: relative; display: flex; align-items: center; gap: 11px; padding: 9px 12px; margin: 0; }
|
|
1268
|
+
.lc-mock-hud-row + .lc-mock-hud-row::before {
|
|
1269
|
+
content: ''; position: absolute; top: 0; left: 50px; right: 0; height: 0.5px; background: var(--lc-hairline);
|
|
1270
|
+
}
|
|
1271
|
+
.lc-mock-hud-glyph {
|
|
1272
|
+
width: 28px; height: 28px; border-radius: 8px; flex: none; display: flex; align-items: center; justify-content: center;
|
|
1273
|
+
background: var(--lc-fill); color: var(--lc-ink);
|
|
1274
|
+
}
|
|
1275
|
+
.lc-mock-hud-glyph svg { width: 15px; height: 15px; }
|
|
1276
|
+
.lc-mock-hud-row-text { flex: 1; min-width: 0; }
|
|
1277
|
+
.lc-mock-hud-row-label { font-size: 13px; font-weight: 500; }
|
|
1278
|
+
.lc-mock-hud-row-detail { font-size: 12px; color: var(--lc-ink-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 1px; }
|
|
1279
|
+
.lc-mock-hud-check { color: #10B981; flex: none; }
|
|
1280
|
+
.lc-mock-hud-check svg { width: 16px; height: 16px; stroke-width: 2.4; }
|
|
1281
|
+
.lc-mock-hud-empty { padding: 14px; font-size: 13px; color: var(--lc-ink-3); line-height: 1.45; }
|
|
1282
|
+
|
|
1283
|
+
.lc-mock-hud-footer { display: flex; gap: 8px; padding: 12px 2px 0; }
|
|
1284
|
+
.lc-mock-hud-publish {
|
|
1285
|
+
flex: 1; height: 42px; border-radius: 999px; display: inline-flex; align-items: center; justify-content: center; gap: 7px;
|
|
1286
|
+
background: var(--lc-solid); color: var(--lc-on-solid) !important; text-decoration: none !important;
|
|
1287
|
+
font-size: 14px; font-weight: 600; transition: transform 160ms ease, opacity 160ms ease;
|
|
1288
|
+
}
|
|
1289
|
+
.lc-mock-hud-publish svg { width: 15px; height: 15px; }
|
|
1290
|
+
.lc-mock-hud-publish:hover { opacity: 0.9; }
|
|
1291
|
+
.lc-mock-hud-publish:active { transform: scale(0.98); }
|
|
1292
|
+
.lc-mock-hud-copy {
|
|
1293
|
+
width: 42px; height: 42px; padding: 0; border: 0; border-radius: 999px; flex: none; cursor: pointer;
|
|
1294
|
+
display: flex; align-items: center; justify-content: center;
|
|
1295
|
+
background: var(--lc-fill); color: var(--lc-ink); transition: background 160ms ease, color 160ms ease, transform 160ms ease;
|
|
1296
|
+
}
|
|
1297
|
+
.lc-mock-hud-copy svg { width: 17px; height: 17px; }
|
|
1298
|
+
.lc-mock-hud-copy:hover { background: var(--lc-fill-hover); }
|
|
1299
|
+
.lc-mock-hud-copy:active { transform: scale(0.94); }
|
|
1300
|
+
.lc-mock-hud-copy.is-copied { background: rgba(16,185,129,0.14); color: #059669; }
|
|
1301
|
+
.lc-mock-hud .lc-mock-hud-reset { display: block; margin: 10px auto 0; border: 0; background: transparent; color: var(--lc-ink-3); font-size: 12px; cursor: pointer; }
|
|
1302
|
+
.lc-mock-hud .lc-mock-hud-reset:hover { color: var(--lc-ink); }
|
|
1303
|
+
.lc-mock-hud .lc-mock-hud-confirm { padding: 12px 8px; color: var(--lc-ink); }
|
|
1304
|
+
.lc-mock-hud .lc-mock-hud-confirm p { color: var(--lc-ink-3); font-size: 13px; line-height: 1.5; }
|
|
1305
|
+
.lc-mock-hud .lc-mock-hud-confirm-actions { display: flex; gap: 8px; margin-top: 16px; }
|
|
1306
|
+
.lc-mock-hud .lc-mock-hud-confirm button { border-radius: 999px; padding: 10px 16px; border: 1px solid var(--lc-ink-3); background: transparent; color: var(--lc-ink); cursor: pointer; }
|
|
1307
|
+
.lc-mock-hud .lc-mock-hud-confirm .lc-mock-hud-reset-confirm { background: #DC2626; color: white; border-color: #DC2626; }
|
|
1308
|
+
.lc-mock-hud-publish-hint { padding: 10px 8px 0; font-size: 11.5px; line-height: 1.45; color: var(--lc-ink-3); overflow-wrap: anywhere; }
|
|
1309
|
+
.lc-mock-hud-publish-hint code {
|
|
1310
|
+
display: inline-block; max-width: 100%; margin-top: 3px; word-break: break-all;
|
|
1311
|
+
font-family: ui-monospace, 'SF Mono', Menlo, monospace; font-size: 11px; padding: 1px 5px; border-radius: 6px;
|
|
1312
|
+
background: var(--lc-fill); color: var(--lc-ink-2);
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
@media (max-width: 640px) {
|
|
1316
|
+
.lc-mock-stack {
|
|
1317
|
+
top: max(12px, calc(env(safe-area-inset-top, 0px) + 8px)); bottom: auto; left: 12px; right: 12px; width: auto;
|
|
1318
|
+
flex-direction: column-reverse;
|
|
1319
|
+
}
|
|
1320
|
+
.lc-mock-toast { animation-name: lc-mock-in-top; }
|
|
1321
|
+
.lc-mock-toast--older { display: none; }
|
|
1322
|
+
.lc-mock-hud { left: 12px; bottom: max(12px, calc(env(safe-area-inset-bottom, 0px) + 8px)); }
|
|
1323
|
+
.lc-mock-hud-card { width: calc(100vw - 24px); }
|
|
1324
|
+
}
|
|
1325
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1326
|
+
.lc-mock-stack *, .lc-mock-hud *, .lc-mock-hud.lc-mock-animate > * { animation: none !important; transition: none !important; }
|
|
1327
|
+
}
|
|
1328
|
+
`.trim();
|
|
1329
|
+
const hasDocument = () => typeof document !== "undefined" && typeof document.createElement === "function";
|
|
1330
|
+
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
1331
|
+
const createIcon = (name) => {
|
|
1332
|
+
const svg = document.createElementNS(SVG_NS, "svg");
|
|
1333
|
+
svg.setAttribute("viewBox", "0 0 24 24");
|
|
1334
|
+
svg.setAttribute("fill", "none");
|
|
1335
|
+
svg.setAttribute("stroke", "currentColor");
|
|
1336
|
+
svg.setAttribute("stroke-width", "1.9");
|
|
1337
|
+
svg.setAttribute("stroke-linecap", "round");
|
|
1338
|
+
svg.setAttribute("stroke-linejoin", "round");
|
|
1339
|
+
svg.setAttribute("aria-hidden", "true");
|
|
1340
|
+
svg.innerHTML = ICON_PATHS[name];
|
|
1341
|
+
return svg;
|
|
1342
|
+
};
|
|
1343
|
+
const createEl = (tag, className, text) => {
|
|
1344
|
+
const el = document.createElement(tag);
|
|
1345
|
+
if (className) el.className = className;
|
|
1346
|
+
if (text !== void 0) el.textContent = text;
|
|
1347
|
+
return el;
|
|
1348
|
+
};
|
|
1349
|
+
const plural = (count, noun) => `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
1350
|
+
const readablePath = (path) => path.split(/[?#]/)[0] || path;
|
|
1351
|
+
const readHost = (url) => {
|
|
1352
|
+
if (!url) return "";
|
|
1353
|
+
try {
|
|
1354
|
+
return new URL(url).host;
|
|
1355
|
+
} catch {
|
|
1356
|
+
return "";
|
|
1357
|
+
}
|
|
1358
|
+
};
|
|
1359
|
+
const readableTemplateName = (record) => {
|
|
1360
|
+
const template = record.template;
|
|
1361
|
+
const name = template == null ? void 0 : template.name;
|
|
1362
|
+
return typeof name === "string" && name.trim() && !name.includes("{{") ? name.trim() : record.alias;
|
|
1363
|
+
};
|
|
1364
|
+
const MAX_APP_FINGERPRINT_LENGTH = 64;
|
|
1365
|
+
const slugifyAppFingerprint = (value) => {
|
|
1366
|
+
if (!value) return "default";
|
|
1367
|
+
const slug = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, MAX_APP_FINGERPRINT_LENGTH).replace(/-$/g, "");
|
|
1368
|
+
return slug || "default";
|
|
1369
|
+
};
|
|
1370
|
+
const readAchievementName = (payload) => {
|
|
1371
|
+
var _a, _b, _c, _d;
|
|
1372
|
+
if (!payload || typeof payload !== "object") return "a credential";
|
|
1373
|
+
const record = payload;
|
|
1374
|
+
if (typeof record.templateAlias === "string" && record.templateAlias) {
|
|
50
1375
|
const data = record.templateData;
|
|
51
1376
|
if (data && typeof data === "object") {
|
|
52
1377
|
const fields = data;
|
|
@@ -66,13 +1391,70 @@ const readAchievementName = (payload) => {
|
|
|
66
1391
|
}
|
|
67
1392
|
return "a credential";
|
|
68
1393
|
};
|
|
1394
|
+
const readRenderedCredentialName = (credential) => {
|
|
1395
|
+
if (typeof credential.name === "string" && credential.name) return credential.name;
|
|
1396
|
+
const subject = credential.credentialSubject;
|
|
1397
|
+
if (subject && typeof subject === "object" && !Array.isArray(subject)) {
|
|
1398
|
+
const achievement = subject.achievement;
|
|
1399
|
+
if (achievement && typeof achievement === "object" && !Array.isArray(achievement)) {
|
|
1400
|
+
const name = achievement.name;
|
|
1401
|
+
if (typeof name === "string" && name) return name;
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
return "a credential";
|
|
1405
|
+
};
|
|
1406
|
+
const buildConsentScopeToastSegments = (payload) => {
|
|
1407
|
+
const scopes = payload == null ? void 0 : payload.scopes;
|
|
1408
|
+
if (!scopes || typeof scopes !== "object" || Array.isArray(scopes)) {
|
|
1409
|
+
return [
|
|
1410
|
+
"In LearnCard, the learner would review this request. Approved automatically here."
|
|
1411
|
+
];
|
|
1412
|
+
}
|
|
1413
|
+
const normalized = normalizeConsentRequest(scopes);
|
|
1414
|
+
const readCategories = normalized.read.credentialCategories;
|
|
1415
|
+
const personalFields = normalized.read.personalFields;
|
|
1416
|
+
const writeCategories = normalized.write.credentialCategories;
|
|
1417
|
+
const asks = [];
|
|
1418
|
+
if (readCategories.length > 0) {
|
|
1419
|
+
asks.push(["see their ", { b: readCategories.join(", ") }, " credentials"]);
|
|
1420
|
+
}
|
|
1421
|
+
if (personalFields.length > 0) {
|
|
1422
|
+
asks.push(["see their ", { b: personalFields.join(", ") }]);
|
|
1423
|
+
}
|
|
1424
|
+
if (writeCategories.length > 0) {
|
|
1425
|
+
asks.push(["add ", { b: writeCategories.join(", ") }, " credentials"]);
|
|
1426
|
+
}
|
|
1427
|
+
if (asks.length === 0) {
|
|
1428
|
+
return ["The learner would be asked for permission. Approved automatically here."];
|
|
1429
|
+
}
|
|
1430
|
+
const parts = ["The learner would be asked to let your app "];
|
|
1431
|
+
asks.forEach((ask, index) => {
|
|
1432
|
+
if (index > 0) parts.push(index === asks.length - 1 ? " and " : ", ");
|
|
1433
|
+
parts.push(...ask);
|
|
1434
|
+
});
|
|
1435
|
+
parts.push(". Approved automatically here.");
|
|
1436
|
+
return parts;
|
|
1437
|
+
};
|
|
69
1438
|
class MockHost {
|
|
70
|
-
constructor(options) {
|
|
1439
|
+
constructor(options, context) {
|
|
71
1440
|
__publicField$1(this, "options");
|
|
1441
|
+
__publicField$1(this, "context");
|
|
72
1442
|
/** In-memory counter fallback used when persistence is off/unavailable. */
|
|
73
1443
|
__publicField$1(this, "memoryCounters", /* @__PURE__ */ new Map());
|
|
1444
|
+
__publicField$1(this, "memoryManifests", {});
|
|
1445
|
+
__publicField$1(this, "memoryPublishDismissedAt", {});
|
|
1446
|
+
__publicField$1(this, "memoryHudCollapsed", {});
|
|
1447
|
+
__publicField$1(this, "initialDocumentTitle");
|
|
1448
|
+
__publicField$1(this, "appFingerprint");
|
|
1449
|
+
__publicField$1(this, "memoryAppKey");
|
|
1450
|
+
__publicField$1(this, "iconSource");
|
|
1451
|
+
__publicField$1(this, "iconGeneration", 0);
|
|
1452
|
+
__publicField$1(this, "iconCleanups", /* @__PURE__ */ new Set());
|
|
1453
|
+
__publicField$1(this, "confirmingReset", false);
|
|
74
1454
|
/** Session-scoped credential store; reads reflect writes and seeds. */
|
|
75
1455
|
__publicField$1(this, "credentials", []);
|
|
1456
|
+
/** Session-scoped alias → inline template version state. */
|
|
1457
|
+
__publicField$1(this, "inlineTemplateVersions", /* @__PURE__ */ new Map());
|
|
76
1458
|
__publicField$1(this, "identity");
|
|
77
1459
|
/** DOM nodes injected for visual feedback, tracked for cleanup. */
|
|
78
1460
|
__publicField$1(this, "domNodes", /* @__PURE__ */ new Set());
|
|
@@ -80,23 +1462,30 @@ class MockHost {
|
|
|
80
1462
|
__publicField$1(this, "aiTopic", null);
|
|
81
1463
|
__publicField$1(this, "styleEl", null);
|
|
82
1464
|
__publicField$1(this, "stackEl", null);
|
|
1465
|
+
__publicField$1(this, "hudEl", null);
|
|
1466
|
+
__publicField$1(this, "hudAnimateNext", true);
|
|
83
1467
|
__publicField$1(this, "activeToasts", /* @__PURE__ */ new Map());
|
|
84
1468
|
/** Pending exit-animation timers, tracked so `destroy()` can cancel them. */
|
|
85
1469
|
__publicField$1(this, "exitTimers", /* @__PURE__ */ new Set());
|
|
86
1470
|
__publicField$1(this, "idSeq", 0);
|
|
87
1471
|
__publicField$1(this, "destroyed", false);
|
|
88
|
-
|
|
1472
|
+
__publicField$1(this, "publishPromptShown", false);
|
|
1473
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
1474
|
+
this.context = { ...context };
|
|
89
1475
|
this.options = {
|
|
90
1476
|
ui: (_a = options == null ? void 0 : options.ui) != null ? _a : true,
|
|
91
1477
|
log: (_b = options == null ? void 0 : options.log) != null ? _b : true,
|
|
92
1478
|
persist: (_c = options == null ? void 0 : options.persist) != null ? _c : true,
|
|
93
|
-
namespace: (options == null ? void 0 : options.namespace) || DEFAULT_NAMESPACE
|
|
1479
|
+
namespace: (options == null ? void 0 : options.namespace) || DEFAULT_NAMESPACE,
|
|
1480
|
+
appId: options == null ? void 0 : options.appId,
|
|
1481
|
+
publishPrompt: (_d = options == null ? void 0 : options.publishPrompt) != null ? _d : true,
|
|
1482
|
+
publishOrigin: (options == null ? void 0 : options.publishOrigin) || DEFAULT_PUBLISH_ORIGIN$1
|
|
94
1483
|
};
|
|
95
1484
|
this.identity = {
|
|
96
|
-
...(
|
|
97
|
-
did: ((
|
|
1485
|
+
...(_e = options == null ? void 0 : options.identity) != null ? _e : {},
|
|
1486
|
+
did: ((_f = options == null ? void 0 : options.identity) == null ? void 0 : _f.did) || (options == null ? void 0 : options.did) || DEFAULT_DID
|
|
98
1487
|
};
|
|
99
|
-
for (const seed of (
|
|
1488
|
+
for (const seed of (_g = options == null ? void 0 : options.credentials) != null ? _g : []) {
|
|
100
1489
|
this.addCredential({
|
|
101
1490
|
name: seed.name || seed.templateAlias || "Mock Credential",
|
|
102
1491
|
templateAlias: seed.templateAlias,
|
|
@@ -105,7 +1494,7 @@ class MockHost {
|
|
|
105
1494
|
status: seed.status
|
|
106
1495
|
});
|
|
107
1496
|
}
|
|
108
|
-
for (const [key, value] of Object.entries((
|
|
1497
|
+
for (const [key, value] of Object.entries((_h = options == null ? void 0 : options.counters) != null ? _h : {})) {
|
|
109
1498
|
if (this.readCounter(key) === void 0) this.writeCounter(key, value);
|
|
110
1499
|
}
|
|
111
1500
|
this.announce();
|
|
@@ -115,346 +1504,1447 @@ class MockHost {
|
|
|
115
1504
|
* direct actions map 1:1, and `APP_EVENT` is dispatched by its `type`.
|
|
116
1505
|
*/
|
|
117
1506
|
handle(action, payload) {
|
|
118
|
-
var _a, _b;
|
|
119
1507
|
if (this.destroyed) {
|
|
120
|
-
return Promise.reject(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
1508
|
+
return Promise.reject(
|
|
1509
|
+
new PartnerConnectError(
|
|
1510
|
+
"SDK_DESTROYED",
|
|
1511
|
+
"Mock host was destroyed before the request completed"
|
|
1512
|
+
)
|
|
1513
|
+
);
|
|
124
1514
|
}
|
|
125
1515
|
this.log(action, payload);
|
|
126
1516
|
if (action === "APP_EVENT") {
|
|
127
1517
|
return this.handleAppEvent(payload);
|
|
128
1518
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
1519
|
+
const response = (() => {
|
|
1520
|
+
var _a, _b;
|
|
1521
|
+
switch (action) {
|
|
1522
|
+
case "REQUEST_IDENTITY":
|
|
1523
|
+
this.toast({
|
|
1524
|
+
icon: "user",
|
|
1525
|
+
segments: [
|
|
1526
|
+
"In LearnCard, the learner would sign in. Using a practice profile for now."
|
|
1527
|
+
]
|
|
1528
|
+
});
|
|
1529
|
+
return Promise.resolve({
|
|
1530
|
+
token: `mock-token-${Date.now()}`,
|
|
1531
|
+
user: { ...this.identity }
|
|
1532
|
+
});
|
|
1533
|
+
case "SEND_CREDENTIAL": {
|
|
1534
|
+
const name = readAchievementName(payload);
|
|
1535
|
+
const credentialId = `mock-credential-${Date.now()}-${this.idSeq += 1}`;
|
|
1536
|
+
this.addCredential({
|
|
1537
|
+
name,
|
|
1538
|
+
credentialUri: credentialId,
|
|
1539
|
+
credential: payload == null ? void 0 : payload.credential
|
|
1540
|
+
});
|
|
1541
|
+
this.showClaimToast(name);
|
|
1542
|
+
return Promise.resolve({ credentialId, stored: true });
|
|
1543
|
+
}
|
|
1544
|
+
case "REQUEST_CONSENT": {
|
|
1545
|
+
const redirect = Boolean(
|
|
1546
|
+
payload == null ? void 0 : payload.redirect
|
|
1547
|
+
);
|
|
1548
|
+
try {
|
|
1549
|
+
const scopes = payload == null ? void 0 : payload.scopes;
|
|
1550
|
+
if (scopes !== void 0) {
|
|
1551
|
+
normalizeConsentRequest(scopes);
|
|
1552
|
+
}
|
|
1553
|
+
} catch (error) {
|
|
1554
|
+
return Promise.reject(
|
|
1555
|
+
new PartnerConnectError(
|
|
1556
|
+
"CONSENT_SCOPES_INVALID",
|
|
1557
|
+
error instanceof Error ? error.message : "Invalid consent scopes"
|
|
1558
|
+
)
|
|
1559
|
+
);
|
|
1560
|
+
}
|
|
1561
|
+
this.showConsentToast(payload, redirect);
|
|
1562
|
+
return Promise.resolve({ granted: true });
|
|
1563
|
+
}
|
|
1564
|
+
case "LAUNCH_FEATURE": {
|
|
1565
|
+
const featurePath = (_a = payload == null ? void 0 : payload.featurePath) != null ? _a : "";
|
|
1566
|
+
this.toast({
|
|
1567
|
+
icon: "open",
|
|
1568
|
+
segments: featurePath ? [
|
|
1569
|
+
"In LearnCard, this would open ",
|
|
1570
|
+
{ b: readablePath(featurePath) },
|
|
1571
|
+
"."
|
|
1572
|
+
] : ["In LearnCard, this would open a feature screen."]
|
|
1573
|
+
});
|
|
1574
|
+
return Promise.resolve({ launched: true, featurePath });
|
|
1575
|
+
}
|
|
1576
|
+
case "ASK_CREDENTIAL_SEARCH": {
|
|
1577
|
+
const held = this.selfCredentials();
|
|
1578
|
+
this.toast({
|
|
1579
|
+
icon: "search",
|
|
1580
|
+
segments: held.length ? [
|
|
1581
|
+
"The learner could share ",
|
|
1582
|
+
{ b: plural(held.length, "credential") },
|
|
1583
|
+
"."
|
|
1584
|
+
] : [
|
|
1585
|
+
"In LearnCard, the learner would choose credentials to share. None yet in practice."
|
|
1586
|
+
]
|
|
1587
|
+
});
|
|
1588
|
+
return Promise.resolve({
|
|
1589
|
+
verifiablePresentation: {
|
|
1590
|
+
verifiableCredential: held.map((c) => c.credential)
|
|
1591
|
+
}
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
case "ASK_CREDENTIAL_SPECIFIC": {
|
|
1595
|
+
const credentialId = payload == null ? void 0 : payload.credentialId;
|
|
1596
|
+
const found = this.credentials.find((c) => c.credentialUri === credentialId);
|
|
1597
|
+
this.toast({
|
|
1598
|
+
icon: "search",
|
|
1599
|
+
segments: found ? ["The learner would share ", { b: found.name }, "."] : [
|
|
1600
|
+
"In LearnCard, the learner would be asked to share this credential. It isn\u2019t in practice data yet."
|
|
1601
|
+
]
|
|
1602
|
+
});
|
|
1603
|
+
return Promise.resolve({ credential: found == null ? void 0 : found.credential });
|
|
1604
|
+
}
|
|
1605
|
+
case "INITIATE_TEMPLATE_ISSUE": {
|
|
1606
|
+
const input = payload;
|
|
1607
|
+
const templateId = (_b = input == null ? void 0 : input.templateId) != null ? _b : "";
|
|
1608
|
+
const recipients = Array.isArray(input == null ? void 0 : input.draftRecipients) ? input == null ? void 0 : input.draftRecipients : [];
|
|
1609
|
+
for (const recipient of recipients) {
|
|
1610
|
+
this.addCredential({
|
|
1611
|
+
name: templateId || "Boost",
|
|
1612
|
+
boostUri: templateId,
|
|
1613
|
+
recipient,
|
|
1614
|
+
status: "pending"
|
|
1615
|
+
});
|
|
1616
|
+
}
|
|
1617
|
+
this.toast({
|
|
1618
|
+
icon: "send",
|
|
1619
|
+
segments: recipients.length ? [
|
|
1620
|
+
"In LearnCard, this would send to ",
|
|
1621
|
+
{ b: plural(recipients.length, "person") },
|
|
1622
|
+
"."
|
|
1623
|
+
] : ["In LearnCard, this would open the send screen."]
|
|
1624
|
+
});
|
|
1625
|
+
return Promise.resolve({ issued: true });
|
|
1626
|
+
}
|
|
1627
|
+
case "REQUEST_LEARNER_CONTEXT": {
|
|
1628
|
+
const opts = payload != null ? payload : {};
|
|
1629
|
+
const includeCredentials = opts.includeCredentials !== false;
|
|
1630
|
+
const structured = opts.format === "structured";
|
|
1631
|
+
const held = includeCredentials ? this.selfCredentials() : [];
|
|
1632
|
+
this.toast({
|
|
1633
|
+
icon: "sparkles",
|
|
1634
|
+
segments: !includeCredentials ? [
|
|
1635
|
+
"In LearnCard, the learner\u2019s profile would load, without credentials."
|
|
1636
|
+
] : held.length ? [
|
|
1637
|
+
"Learner profile loaded with ",
|
|
1638
|
+
{ b: plural(held.length, "credential") },
|
|
1639
|
+
"."
|
|
1640
|
+
] : [
|
|
1641
|
+
"In LearnCard, the learner\u2019s profile would load here. Empty in practice."
|
|
1642
|
+
]
|
|
1643
|
+
});
|
|
1644
|
+
const prompt = !includeCredentials ? "Mock learner context: credentials were not requested." : held.length ? `Mock learner context. Credentials held: ${held.map((c) => c.name).join(", ")}.` : "Mock learner context: this user has no credentials in standalone mode.";
|
|
1645
|
+
return Promise.resolve({
|
|
1646
|
+
status: "ready",
|
|
1647
|
+
prompt,
|
|
1648
|
+
did: this.identity.did,
|
|
1649
|
+
// Matches the real host: `raw` only ships for format: 'structured'.
|
|
1650
|
+
...structured ? { raw: { credentials: held.map((c) => c.credential) } } : {}
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
case "GET_SYNC_STATUS":
|
|
1654
|
+
this.toast({
|
|
1655
|
+
icon: "sync",
|
|
1656
|
+
segments: [
|
|
1657
|
+
"In LearnCard, this checks that the learner\u2019s data is up to date. Ready in practice."
|
|
1658
|
+
]
|
|
1659
|
+
});
|
|
1660
|
+
return Promise.resolve({
|
|
1661
|
+
status: "ready",
|
|
1662
|
+
progress: {
|
|
1663
|
+
totalCredentials: 0,
|
|
1664
|
+
completedCredentials: 0,
|
|
1665
|
+
failedCredentials: 0,
|
|
1666
|
+
retryCount: 0
|
|
1667
|
+
}
|
|
1668
|
+
});
|
|
1669
|
+
default:
|
|
1670
|
+
this.toast({
|
|
1671
|
+
icon: "sparkles",
|
|
1672
|
+
segments: ["In LearnCard, this would run ", { b: action }, "."]
|
|
1673
|
+
});
|
|
1674
|
+
return Promise.resolve({});
|
|
149
1675
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
1676
|
+
})();
|
|
1677
|
+
return response.then((result) => {
|
|
1678
|
+
this.captureHandledAction(action, payload);
|
|
1679
|
+
return result;
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
/** Tear down injected UI and clear in-memory state. */
|
|
1683
|
+
destroy() {
|
|
1684
|
+
this.destroyed = true;
|
|
1685
|
+
this.cancelIconCapture();
|
|
1686
|
+
for (const entry of this.activeToasts.values()) {
|
|
1687
|
+
if (entry.timeoutId) clearTimeout(entry.timeoutId);
|
|
1688
|
+
}
|
|
1689
|
+
this.activeToasts.clear();
|
|
1690
|
+
for (const timer of this.exitTimers) clearTimeout(timer);
|
|
1691
|
+
this.exitTimers.clear();
|
|
1692
|
+
for (const node of this.domNodes) node.remove();
|
|
1693
|
+
this.domNodes.clear();
|
|
1694
|
+
if (this.stackEl) {
|
|
1695
|
+
this.stackEl.remove();
|
|
1696
|
+
this.stackEl = null;
|
|
1697
|
+
}
|
|
1698
|
+
if (this.styleEl) {
|
|
1699
|
+
this.styleEl.remove();
|
|
1700
|
+
this.styleEl = null;
|
|
1701
|
+
}
|
|
1702
|
+
this.memoryCounters.clear();
|
|
1703
|
+
for (const key of Object.keys(this.memoryManifests)) delete this.memoryManifests[key];
|
|
1704
|
+
for (const key of Object.keys(this.memoryPublishDismissedAt)) {
|
|
1705
|
+
delete this.memoryPublishDismissedAt[key];
|
|
1706
|
+
}
|
|
1707
|
+
for (const key of Object.keys(this.memoryHudCollapsed)) delete this.memoryHudCollapsed[key];
|
|
1708
|
+
this.credentials.length = 0;
|
|
1709
|
+
}
|
|
1710
|
+
getCapturedManifest() {
|
|
1711
|
+
const manifest = this.loadManifest();
|
|
1712
|
+
return manifest ? this.cloneManifest(manifest) : void 0;
|
|
1713
|
+
}
|
|
1714
|
+
getPublishOrigin() {
|
|
1715
|
+
return this.options.publishOrigin;
|
|
1716
|
+
}
|
|
1717
|
+
resetPracticeMode() {
|
|
1718
|
+
if (this.destroyed) return;
|
|
1719
|
+
this.cancelIconCapture();
|
|
1720
|
+
const manifests = this.loadManifestMap();
|
|
1721
|
+
delete manifests[this.getAppFingerprint()];
|
|
1722
|
+
this.replaceMemoryManifestMap(manifests);
|
|
1723
|
+
if (this.options.persist) {
|
|
1724
|
+
try {
|
|
1725
|
+
localStorage.setItem(this.manifestStorageKey(), JSON.stringify(manifests));
|
|
1726
|
+
for (const key of [
|
|
1727
|
+
this.counterStorageKey(),
|
|
1728
|
+
this.publishDismissedStorageKey(),
|
|
1729
|
+
this.appKeyStorageKey()
|
|
1730
|
+
]) {
|
|
1731
|
+
localStorage.removeItem(key);
|
|
1732
|
+
}
|
|
1733
|
+
} catch {
|
|
154
1734
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
1735
|
+
}
|
|
1736
|
+
this.memoryCounters.clear();
|
|
1737
|
+
this.memoryAppKey = void 0;
|
|
1738
|
+
delete this.memoryPublishDismissedAt[this.getAppFingerprint()];
|
|
1739
|
+
this.credentials.length = 0;
|
|
1740
|
+
this.inlineTemplateVersions.clear();
|
|
1741
|
+
this.aiTopic = null;
|
|
1742
|
+
this.publishPromptShown = false;
|
|
1743
|
+
this.confirmingReset = false;
|
|
1744
|
+
for (const [key, entry] of this.activeToasts) {
|
|
1745
|
+
if (entry.node.classList.contains("lc-mock-toast--publish")) this.dismissToast(key);
|
|
1746
|
+
}
|
|
1747
|
+
this.saveHudCollapsed(true);
|
|
1748
|
+
this.updateManifestHud();
|
|
1749
|
+
this.toast({ icon: "sync", segments: ["Started fresh."] });
|
|
1750
|
+
}
|
|
1751
|
+
handleAppEvent(event) {
|
|
1752
|
+
const type = typeof (event == null ? void 0 : event.type) === "string" ? event.type : "";
|
|
1753
|
+
const response = (() => {
|
|
1754
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
|
|
1755
|
+
switch (type) {
|
|
1756
|
+
case "send-credential": {
|
|
1757
|
+
if (typeof event.alias === "string" && event.alias && event.template && typeof event.template === "object") {
|
|
1758
|
+
const alias = event.alias;
|
|
1759
|
+
const template = event.template;
|
|
1760
|
+
const templateErrors = validateInlineTemplate(template);
|
|
1761
|
+
if (templateErrors.length > 0) {
|
|
1762
|
+
return Promise.reject(
|
|
1763
|
+
new PartnerConnectError(
|
|
1764
|
+
"TEMPLATE_INVALID",
|
|
1765
|
+
templateErrors.map((error) => `${error.path}: ${error.message}`).join("\n")
|
|
1766
|
+
)
|
|
1767
|
+
);
|
|
1768
|
+
}
|
|
1769
|
+
const compiled = compileInlineTemplate(template);
|
|
1770
|
+
const templateData = typeof event.templateData === "object" && event.templateData !== null ? event.templateData : void 0;
|
|
1771
|
+
const dataErrors = validateTemplateData(
|
|
1772
|
+
compiled.variableManifest,
|
|
1773
|
+
templateData
|
|
1774
|
+
);
|
|
1775
|
+
if (dataErrors.length > 0) {
|
|
1776
|
+
return Promise.reject(
|
|
1777
|
+
new PartnerConnectError(
|
|
1778
|
+
"TEMPLATE_DATA_INVALID",
|
|
1779
|
+
dataErrors.map((error) => `${error.path}: ${error.message}`).join("\n")
|
|
1780
|
+
)
|
|
1781
|
+
);
|
|
1782
|
+
}
|
|
1783
|
+
const canonicalTemplate = canonicalJsonString(template);
|
|
1784
|
+
const previousState = this.inlineTemplateVersions.get(alias);
|
|
1785
|
+
const templateVersion = previousState ? previousState.canonicalTemplate === canonicalTemplate ? previousState.version : previousState.version + 1 : 1;
|
|
1786
|
+
this.inlineTemplateVersions.set(alias, {
|
|
1787
|
+
canonicalTemplate,
|
|
1788
|
+
version: templateVersion
|
|
1789
|
+
});
|
|
1790
|
+
const boostUri2 = `lc:mock:inline-boost:${alias}`;
|
|
1791
|
+
if (event.preventDuplicateClaim) {
|
|
1792
|
+
const existing = this.selfCredentials().find(
|
|
1793
|
+
(c) => this.matchesTemplate(c, { templateAlias: alias, boostUri: boostUri2 })
|
|
1794
|
+
);
|
|
1795
|
+
if (existing) {
|
|
1796
|
+
this.toast({
|
|
1797
|
+
icon: "award",
|
|
1798
|
+
segments: [
|
|
1799
|
+
"The learner already has ",
|
|
1800
|
+
{ b: existing.name },
|
|
1801
|
+
"."
|
|
1802
|
+
]
|
|
1803
|
+
});
|
|
1804
|
+
return Promise.resolve({
|
|
1805
|
+
credentialUri: existing.credentialUri,
|
|
1806
|
+
boostUri: (_a = existing.boostUri) != null ? _a : boostUri2,
|
|
1807
|
+
alreadyClaimed: true,
|
|
1808
|
+
hasCredential: true,
|
|
1809
|
+
status: existing.status,
|
|
1810
|
+
receivedDate: existing.receivedDate,
|
|
1811
|
+
templateVersion
|
|
1812
|
+
});
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
const renderedCredential = renderCompiledTemplate(
|
|
1816
|
+
compiled.credentialTemplateJson,
|
|
1817
|
+
{
|
|
1818
|
+
...templateData != null ? templateData : {},
|
|
1819
|
+
issue_date: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1820
|
+
issuer_did: "did:web:mock.learncard.app:app",
|
|
1821
|
+
recipient_did: this.identity.did
|
|
1822
|
+
}
|
|
1823
|
+
);
|
|
1824
|
+
renderedCredential._mock = true;
|
|
1825
|
+
const renderedName = readRenderedCredentialName(renderedCredential);
|
|
1826
|
+
const record2 = this.addCredential({
|
|
1827
|
+
name: renderedName,
|
|
1828
|
+
templateAlias: alias,
|
|
1829
|
+
boostUri: boostUri2,
|
|
1830
|
+
status: "claimed",
|
|
1831
|
+
credential: renderedCredential
|
|
1832
|
+
});
|
|
1833
|
+
this.showClaimToast(renderedName, templateVersion);
|
|
1834
|
+
return Promise.resolve({
|
|
1835
|
+
credentialUri: record2.credentialUri,
|
|
1836
|
+
boostUri: (_b = record2.boostUri) != null ? _b : boostUri2,
|
|
1837
|
+
alreadyClaimed: false,
|
|
1838
|
+
hasCredential: true,
|
|
1839
|
+
status: "claimed",
|
|
1840
|
+
receivedDate: record2.receivedDate,
|
|
1841
|
+
templateVersion
|
|
1842
|
+
});
|
|
1843
|
+
}
|
|
1844
|
+
const name = readAchievementName(event);
|
|
1845
|
+
const templateAlias = typeof event.templateAlias === "string" ? event.templateAlias : void 0;
|
|
1846
|
+
const boostUri = `lc:mock:boost:${String((_c = event.templateAlias) != null ? _c : "template")}`;
|
|
1847
|
+
if (event.preventDuplicateClaim) {
|
|
1848
|
+
const existing = this.selfCredentials().find(
|
|
1849
|
+
(c) => this.matchesTemplate(c, { templateAlias, boostUri })
|
|
1850
|
+
);
|
|
1851
|
+
if (existing) {
|
|
1852
|
+
this.toast({
|
|
1853
|
+
icon: "award",
|
|
1854
|
+
segments: ["The learner already has ", { b: existing.name }, "."]
|
|
1855
|
+
});
|
|
1856
|
+
return Promise.resolve({
|
|
1857
|
+
credentialUri: existing.credentialUri,
|
|
1858
|
+
boostUri: (_d = existing.boostUri) != null ? _d : boostUri,
|
|
1859
|
+
alreadyClaimed: true,
|
|
1860
|
+
hasCredential: true,
|
|
1861
|
+
status: existing.status,
|
|
1862
|
+
receivedDate: existing.receivedDate
|
|
1863
|
+
});
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
const record = this.addCredential({
|
|
1867
|
+
name,
|
|
1868
|
+
templateAlias,
|
|
1869
|
+
boostUri,
|
|
1870
|
+
status: "claimed"
|
|
1871
|
+
});
|
|
1872
|
+
this.showClaimToast(name);
|
|
1873
|
+
return Promise.resolve({
|
|
1874
|
+
credentialUri: record.credentialUri,
|
|
1875
|
+
boostUri: (_e = record.boostUri) != null ? _e : boostUri,
|
|
1876
|
+
alreadyClaimed: false,
|
|
1877
|
+
hasCredential: true,
|
|
1878
|
+
status: "claimed",
|
|
1879
|
+
receivedDate: record.receivedDate
|
|
1880
|
+
});
|
|
1881
|
+
}
|
|
1882
|
+
case "check-credential": {
|
|
1883
|
+
const held = this.selfCredentials().find((c) => this.matchesTemplate(c, event));
|
|
1884
|
+
this.toast({
|
|
1885
|
+
icon: "search",
|
|
1886
|
+
segments: held ? ["The learner already has ", { b: held.name }, "."] : ["The learner doesn\u2019t have this credential yet."]
|
|
1887
|
+
});
|
|
1888
|
+
return Promise.resolve(
|
|
1889
|
+
held ? {
|
|
1890
|
+
hasCredential: true,
|
|
1891
|
+
credentialUri: held.credentialUri,
|
|
1892
|
+
receivedDate: held.receivedDate,
|
|
1893
|
+
status: held.status
|
|
1894
|
+
} : { hasCredential: false }
|
|
1895
|
+
);
|
|
1896
|
+
}
|
|
1897
|
+
case "check-issuance-status": {
|
|
1898
|
+
const recipient = typeof event.recipient === "string" ? event.recipient : "";
|
|
1899
|
+
const match = this.credentials.find(
|
|
1900
|
+
(c) => this.matchesTemplate(c, event) && c.recipient === recipient
|
|
1901
|
+
);
|
|
1902
|
+
this.toast({
|
|
1903
|
+
icon: "send",
|
|
1904
|
+
segments: match ? ["Sent to ", { b: recipient }, " \u2014 ", { b: match.status }, "."] : ["Not sent to this person yet."]
|
|
1905
|
+
});
|
|
1906
|
+
return Promise.resolve(
|
|
1907
|
+
match ? {
|
|
1908
|
+
sent: true,
|
|
1909
|
+
credentialUri: match.credentialUri,
|
|
1910
|
+
sentDate: match.sentDate,
|
|
1911
|
+
claimedDate: match.claimedDate,
|
|
1912
|
+
status: match.status
|
|
1913
|
+
} : { sent: false }
|
|
1914
|
+
);
|
|
1915
|
+
}
|
|
1916
|
+
case "get-template-recipients": {
|
|
1917
|
+
const matched = this.credentials.filter((c) => this.matchesTemplate(c, event));
|
|
1918
|
+
const limit = typeof event.limit === "number" && event.limit > 0 ? event.limit : void 0;
|
|
1919
|
+
const parsedCursor = typeof event.cursor === "string" ? Number.parseInt(event.cursor, 10) : 0;
|
|
1920
|
+
const offset = Number.isFinite(parsedCursor) && parsedCursor > 0 ? parsedCursor : 0;
|
|
1921
|
+
const page = limit ? matched.slice(offset, offset + limit) : matched.slice(offset);
|
|
1922
|
+
const nextOffset = offset + page.length;
|
|
1923
|
+
const hasMore = nextOffset < matched.length;
|
|
1924
|
+
this.toast({
|
|
1925
|
+
icon: "users",
|
|
1926
|
+
segments: [
|
|
1927
|
+
"In LearnCard, this lists who received it. ",
|
|
1928
|
+
{ b: String(matched.length) },
|
|
1929
|
+
" so far in practice."
|
|
1930
|
+
]
|
|
1931
|
+
});
|
|
1932
|
+
return Promise.resolve({
|
|
1933
|
+
records: page.map((c) => this.toRecipientRecord(c)),
|
|
1934
|
+
hasMore,
|
|
1935
|
+
...hasMore ? { cursor: String(nextOffset) } : {},
|
|
1936
|
+
total: matched.length
|
|
1937
|
+
});
|
|
1938
|
+
}
|
|
1939
|
+
case "send-notification": {
|
|
1940
|
+
const title = typeof event.title === "string" ? event.title : "";
|
|
1941
|
+
const body = typeof event.body === "string" ? event.body : "";
|
|
1942
|
+
const text = [title, body].filter(Boolean).join(" \u2014 ");
|
|
1943
|
+
this.toast({
|
|
1944
|
+
icon: "bell",
|
|
1945
|
+
segments: text ? ["The learner would get a notification: ", { b: text }] : ["In LearnCard, the learner would get a notification."]
|
|
1946
|
+
});
|
|
1947
|
+
return Promise.resolve({ sent: true });
|
|
1948
|
+
}
|
|
1949
|
+
case "increment-counter": {
|
|
1950
|
+
const key = String((_f = event.key) != null ? _f : "");
|
|
1951
|
+
const amount = typeof event.amount === "number" ? event.amount : 0;
|
|
1952
|
+
const previous = (_h = (_g = this.readCounter(key)) == null ? void 0 : _g.value) != null ? _h : 0;
|
|
1953
|
+
const next = previous + amount;
|
|
1954
|
+
this.writeCounter(key, next);
|
|
1955
|
+
this.toast({
|
|
1956
|
+
icon: "hash",
|
|
1957
|
+
segments: ["Counter ", { b: key }, " \u2192 ", { b: String(next) }, "."]
|
|
1958
|
+
});
|
|
1959
|
+
return Promise.resolve({ key, previousValue: previous, newValue: next });
|
|
1960
|
+
}
|
|
1961
|
+
case "get-counter": {
|
|
1962
|
+
const key = String((_i = event.key) != null ? _i : "");
|
|
1963
|
+
const stored = this.readCounter(key);
|
|
1964
|
+
const value = (_j = stored == null ? void 0 : stored.value) != null ? _j : 0;
|
|
1965
|
+
this.toast({
|
|
1966
|
+
icon: "hash",
|
|
1967
|
+
segments: ["Counter ", { b: key }, " is ", { b: String(value) }, "."]
|
|
1968
|
+
});
|
|
1969
|
+
return Promise.resolve({
|
|
1970
|
+
key,
|
|
1971
|
+
value,
|
|
1972
|
+
updatedAt: (_k = stored == null ? void 0 : stored.updatedAt) != null ? _k : null
|
|
1973
|
+
});
|
|
1974
|
+
}
|
|
1975
|
+
case "get-counters": {
|
|
1976
|
+
const requested = Array.isArray(event.keys) ? event.keys.map(String) : this.allCounterKeys();
|
|
1977
|
+
const counters = requested.map((key) => {
|
|
1978
|
+
var _a2, _b2;
|
|
1979
|
+
const stored = this.readCounter(key);
|
|
1980
|
+
return {
|
|
1981
|
+
key,
|
|
1982
|
+
value: (_a2 = stored == null ? void 0 : stored.value) != null ? _a2 : 0,
|
|
1983
|
+
updatedAt: (_b2 = stored == null ? void 0 : stored.updatedAt) != null ? _b2 : null
|
|
1984
|
+
};
|
|
1985
|
+
});
|
|
1986
|
+
this.toast({
|
|
1987
|
+
icon: "hash",
|
|
1988
|
+
segments: ["Read ", { b: plural(counters.length, "counter") }, "."]
|
|
1989
|
+
});
|
|
1990
|
+
return Promise.resolve({ counters });
|
|
1991
|
+
}
|
|
1992
|
+
case "send-ai-session-credential": {
|
|
1993
|
+
const sessionTitle = typeof event.sessionTitle === "string" && event.sessionTitle ? event.sessionTitle : "AI session";
|
|
1994
|
+
const sessionBoostUri = this.nextUri("lc:mock:session-boost");
|
|
1995
|
+
const record = this.addCredential({
|
|
1996
|
+
name: sessionTitle,
|
|
1997
|
+
boostUri: sessionBoostUri,
|
|
1998
|
+
status: "claimed"
|
|
1999
|
+
});
|
|
2000
|
+
this.toast({
|
|
2001
|
+
icon: "award",
|
|
2002
|
+
tone: "positive",
|
|
2003
|
+
segments: [
|
|
2004
|
+
"In LearnCard, the learner would keep a record of ",
|
|
2005
|
+
{ b: sessionTitle },
|
|
2006
|
+
"."
|
|
2007
|
+
]
|
|
2008
|
+
});
|
|
2009
|
+
const isNewTopic = this.aiTopic === null;
|
|
2010
|
+
if (!this.aiTopic) {
|
|
2011
|
+
this.aiTopic = {
|
|
2012
|
+
topicUri: this.nextUri("lc:mock:topic"),
|
|
2013
|
+
topicCredentialUri: this.nextUri("lc:mock:topic-credential")
|
|
2014
|
+
};
|
|
2015
|
+
}
|
|
2016
|
+
return Promise.resolve({
|
|
2017
|
+
topicUri: this.aiTopic.topicUri,
|
|
2018
|
+
...isNewTopic ? { topicCredentialUri: this.aiTopic.topicCredentialUri } : {},
|
|
2019
|
+
sessionCredentialUri: record.credentialUri,
|
|
2020
|
+
sessionBoostUri,
|
|
2021
|
+
isNewTopic
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
2024
|
+
default:
|
|
2025
|
+
this.toast({
|
|
2026
|
+
icon: "sparkles",
|
|
2027
|
+
segments: ["In LearnCard, this would run ", { b: type }, "."]
|
|
2028
|
+
});
|
|
2029
|
+
return Promise.resolve({});
|
|
162
2030
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
2031
|
+
})();
|
|
2032
|
+
return response.then((result) => {
|
|
2033
|
+
this.captureHandledAppEvent(type, event);
|
|
2034
|
+
return result;
|
|
2035
|
+
});
|
|
2036
|
+
}
|
|
2037
|
+
captureHandledAction(action, payload) {
|
|
2038
|
+
try {
|
|
2039
|
+
this.updateManifest((draft) => {
|
|
2040
|
+
switch (action) {
|
|
2041
|
+
case "REQUEST_IDENTITY":
|
|
2042
|
+
this.addPermission(draft, "request_identity");
|
|
2043
|
+
break;
|
|
2044
|
+
case "SEND_CREDENTIAL":
|
|
2045
|
+
this.addPermission(draft, "send_credential");
|
|
2046
|
+
break;
|
|
2047
|
+
case "REQUEST_CONSENT": {
|
|
2048
|
+
this.addPermission(draft, "request_consent");
|
|
2049
|
+
const scopes = payload == null ? void 0 : payload.scopes;
|
|
2050
|
+
if (scopes && typeof scopes === "object" && !Array.isArray(scopes)) {
|
|
2051
|
+
this.captureConsentRecord(draft, scopes);
|
|
2052
|
+
}
|
|
2053
|
+
break;
|
|
174
2054
|
}
|
|
175
|
-
|
|
2055
|
+
case "LAUNCH_FEATURE": {
|
|
2056
|
+
this.addPermission(draft, "launch_feature");
|
|
2057
|
+
const featurePath = payload == null ? void 0 : payload.featurePath;
|
|
2058
|
+
if (typeof featurePath === "string" && featurePath) {
|
|
2059
|
+
this.addUnique(draft.featuresLaunched, featurePath);
|
|
2060
|
+
}
|
|
2061
|
+
break;
|
|
2062
|
+
}
|
|
2063
|
+
case "ASK_CREDENTIAL_SEARCH":
|
|
2064
|
+
this.addPermission(draft, "credential_search");
|
|
2065
|
+
break;
|
|
2066
|
+
case "ASK_CREDENTIAL_SPECIFIC":
|
|
2067
|
+
this.addPermission(draft, "credential_by_id");
|
|
2068
|
+
break;
|
|
2069
|
+
case "INITIATE_TEMPLATE_ISSUE":
|
|
2070
|
+
this.addPermission(draft, "template_issuance");
|
|
2071
|
+
break;
|
|
2072
|
+
case "REQUEST_LEARNER_CONTEXT":
|
|
2073
|
+
draft.usedLearnerContext = true;
|
|
2074
|
+
break;
|
|
2075
|
+
default:
|
|
2076
|
+
break;
|
|
2077
|
+
}
|
|
2078
|
+
});
|
|
2079
|
+
} catch {
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
captureHandledAppEvent(type, event) {
|
|
2083
|
+
try {
|
|
2084
|
+
this.updateManifest((draft) => {
|
|
2085
|
+
var _a, _b;
|
|
2086
|
+
switch (type) {
|
|
2087
|
+
case "send-credential": {
|
|
2088
|
+
this.addPermission(draft, "send_credential");
|
|
2089
|
+
if (typeof event.alias === "string" && event.alias && event.template && typeof event.template === "object") {
|
|
2090
|
+
const version = (_b = (_a = this.inlineTemplateVersions.get(event.alias)) == null ? void 0 : _a.version) != null ? _b : 1;
|
|
2091
|
+
this.captureTemplateRecord(
|
|
2092
|
+
draft,
|
|
2093
|
+
event.alias,
|
|
2094
|
+
event.template,
|
|
2095
|
+
version
|
|
2096
|
+
);
|
|
2097
|
+
}
|
|
2098
|
+
break;
|
|
2099
|
+
}
|
|
2100
|
+
case "send-notification":
|
|
2101
|
+
draft.usedNotifications = true;
|
|
2102
|
+
break;
|
|
2103
|
+
case "increment-counter":
|
|
2104
|
+
case "get-counter": {
|
|
2105
|
+
const key = event.key;
|
|
2106
|
+
if (typeof key === "string" && key) this.addUnique(draft.counterKeys, key);
|
|
2107
|
+
break;
|
|
2108
|
+
}
|
|
2109
|
+
case "get-counters": {
|
|
2110
|
+
if (Array.isArray(event.keys)) {
|
|
2111
|
+
for (const key of event.keys) {
|
|
2112
|
+
if (typeof key === "string" && key) {
|
|
2113
|
+
this.addUnique(draft.counterKeys, key);
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
break;
|
|
2118
|
+
}
|
|
2119
|
+
default:
|
|
2120
|
+
break;
|
|
2121
|
+
}
|
|
2122
|
+
});
|
|
2123
|
+
} catch {
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
updateManifest(mutator) {
|
|
2127
|
+
var _a;
|
|
2128
|
+
const manifest = (_a = this.loadManifest()) != null ? _a : this.createManifestSkeleton();
|
|
2129
|
+
const next = this.cloneManifest(manifest);
|
|
2130
|
+
mutator(next);
|
|
2131
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2132
|
+
next.firstCapturedAt || (next.firstCapturedAt = now);
|
|
2133
|
+
next.lastUpdatedAt = now;
|
|
2134
|
+
this.saveManifest(next);
|
|
2135
|
+
this.updateManifestHud(next);
|
|
2136
|
+
this.refreshPublishPromptLink(next);
|
|
2137
|
+
this.maybeShowPublishPrompt(next);
|
|
2138
|
+
this.captureIcon();
|
|
2139
|
+
}
|
|
2140
|
+
createManifestSkeleton() {
|
|
2141
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2142
|
+
const suggestedName = this.readSuggestedName();
|
|
2143
|
+
const suggestedIconUrl = this.readSuggestedIconUrl();
|
|
2144
|
+
return {
|
|
2145
|
+
manifestVersion: 1,
|
|
2146
|
+
appKey: this.getAppKey(),
|
|
2147
|
+
appUrl: this.readAppOrigin(),
|
|
2148
|
+
...suggestedName ? { suggestedName } : {},
|
|
2149
|
+
...suggestedIconUrl ? { suggestedIconUrl } : {},
|
|
2150
|
+
permissions: [],
|
|
2151
|
+
templates: [],
|
|
2152
|
+
consentRequests: [],
|
|
2153
|
+
featuresLaunched: [],
|
|
2154
|
+
counterKeys: [],
|
|
2155
|
+
usedLearnerContext: false,
|
|
2156
|
+
usedNotifications: false,
|
|
2157
|
+
firstCapturedAt: now,
|
|
2158
|
+
lastUpdatedAt: now
|
|
2159
|
+
};
|
|
2160
|
+
}
|
|
2161
|
+
readAppOrigin() {
|
|
2162
|
+
var _a;
|
|
2163
|
+
if (typeof window === "undefined" || !((_a = window.location) == null ? void 0 : _a.origin)) return "";
|
|
2164
|
+
return `${window.location.origin}${window.location.pathname || ""}`;
|
|
2165
|
+
}
|
|
2166
|
+
readSuggestedName() {
|
|
2167
|
+
const title = this.readInitialDocumentTitle();
|
|
2168
|
+
if (!title) return void 0;
|
|
2169
|
+
const trimmedTitle = title.trim();
|
|
2170
|
+
if (!trimmedTitle) return void 0;
|
|
2171
|
+
return trimmedTitle.slice(0, 100);
|
|
2172
|
+
}
|
|
2173
|
+
readInitialDocumentTitle() {
|
|
2174
|
+
var _a;
|
|
2175
|
+
if (this.initialDocumentTitle !== void 0) {
|
|
2176
|
+
return (_a = this.initialDocumentTitle) != null ? _a : void 0;
|
|
2177
|
+
}
|
|
2178
|
+
if (typeof document === "undefined") {
|
|
2179
|
+
this.initialDocumentTitle = null;
|
|
2180
|
+
return void 0;
|
|
2181
|
+
}
|
|
2182
|
+
this.initialDocumentTitle = document.title;
|
|
2183
|
+
return this.initialDocumentTitle || void 0;
|
|
2184
|
+
}
|
|
2185
|
+
getAppFingerprint() {
|
|
2186
|
+
var _a, _b;
|
|
2187
|
+
if (this.appFingerprint) return this.appFingerprint;
|
|
2188
|
+
this.appFingerprint = slugifyAppFingerprint(
|
|
2189
|
+
(_b = (_a = this.options.appId) != null ? _a : this.readInitialDocumentTitle()) != null ? _b : void 0
|
|
2190
|
+
);
|
|
2191
|
+
return this.appFingerprint;
|
|
2192
|
+
}
|
|
2193
|
+
readLegacyManifestFingerprint(manifest) {
|
|
2194
|
+
return slugifyAppFingerprint(manifest.suggestedName);
|
|
2195
|
+
}
|
|
2196
|
+
readSuggestedIconUrl() {
|
|
2197
|
+
return this.iconCandidates().find((url) => /^https?:/.test(url));
|
|
2198
|
+
}
|
|
2199
|
+
appKeyStorageKey() {
|
|
2200
|
+
return `${this.options.namespace}:app-key:${this.getAppFingerprint()}`;
|
|
2201
|
+
}
|
|
2202
|
+
getAppKey() {
|
|
2203
|
+
var _a, _b, _c, _d;
|
|
2204
|
+
if (this.options.appId !== void 0) return this.options.appId;
|
|
2205
|
+
if (this.options.persist) {
|
|
2206
|
+
try {
|
|
2207
|
+
this.memoryAppKey = localStorage.getItem(this.appKeyStorageKey()) || this.memoryAppKey;
|
|
2208
|
+
} catch {
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
(_d = this.memoryAppKey) != null ? _d : this.memoryAppKey = (_c = (_b = (_a = globalThis.crypto) == null ? void 0 : _a.randomUUID) == null ? void 0 : _b.call(_a)) != null ? _c : `app-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
2212
|
+
if (this.options.persist) {
|
|
2213
|
+
try {
|
|
2214
|
+
localStorage.setItem(this.appKeyStorageKey(), this.memoryAppKey);
|
|
2215
|
+
} catch {
|
|
176
2216
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
2217
|
+
}
|
|
2218
|
+
return this.memoryAppKey;
|
|
2219
|
+
}
|
|
2220
|
+
iconFallback() {
|
|
2221
|
+
return typeof window !== "undefined" ? this.resolveIcon("/favicon.ico", window.location.origin) : void 0;
|
|
2222
|
+
}
|
|
2223
|
+
resolveIcon(href, base) {
|
|
2224
|
+
try {
|
|
2225
|
+
const url = new URL(href, base);
|
|
2226
|
+
return ["http:", "https:", "data:"].includes(url.protocol) ? url.href : void 0;
|
|
2227
|
+
} catch {
|
|
2228
|
+
return void 0;
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
iconSize(sizes, type) {
|
|
2232
|
+
if (sizes.toLowerCase().split(/\s+/).includes("any") || type === "image/svg+xml")
|
|
2233
|
+
return Number.MAX_SAFE_INTEGER;
|
|
2234
|
+
return Math.max(
|
|
2235
|
+
0,
|
|
2236
|
+
...sizes.split(/\s+/).map((size) => {
|
|
2237
|
+
const match = /^(\d+)x(\d+)$/i.exec(size);
|
|
2238
|
+
return match ? Number(match[1]) * Number(match[2]) : 0;
|
|
2239
|
+
})
|
|
2240
|
+
);
|
|
2241
|
+
}
|
|
2242
|
+
iconCandidates() {
|
|
2243
|
+
if (typeof document === "undefined") return [];
|
|
2244
|
+
return Array.from(document.querySelectorAll("link[rel]")).map((link) => {
|
|
2245
|
+
var _a;
|
|
2246
|
+
const rels = link.rel.toLowerCase().split(/\s+/);
|
|
2247
|
+
const priority = rels.includes("mask-icon") ? 0 : rels.some(
|
|
2248
|
+
(rel) => rel === "apple-touch-icon" || rel === "apple-touch-icon-precomposed"
|
|
2249
|
+
) ? 2 : rels.includes("icon") ? 1 : 0;
|
|
2250
|
+
return {
|
|
2251
|
+
priority,
|
|
2252
|
+
size: this.iconSize((_a = link.getAttribute("sizes")) != null ? _a : "", link.type),
|
|
2253
|
+
url: this.resolveIcon(link.href, document.baseURI)
|
|
2254
|
+
};
|
|
2255
|
+
}).filter((candidate) => candidate.priority > 0 && candidate.url).sort((a, b) => b.priority - a.priority || b.size - a.size).flatMap((candidate) => candidate.url ? [candidate.url] : []);
|
|
2256
|
+
}
|
|
2257
|
+
cancelIconCapture() {
|
|
2258
|
+
this.iconGeneration++;
|
|
2259
|
+
for (const cleanup of this.iconCleanups) cleanup();
|
|
2260
|
+
this.iconCleanups.clear();
|
|
2261
|
+
this.iconSource = void 0;
|
|
2262
|
+
}
|
|
2263
|
+
captureIcon() {
|
|
2264
|
+
var _a;
|
|
2265
|
+
if (this.destroyed || typeof document === "undefined") return;
|
|
2266
|
+
const candidates = this.iconCandidates();
|
|
2267
|
+
const manifestHref = (_a = document.querySelector('link[rel~="manifest"]')) == null ? void 0 : _a.href;
|
|
2268
|
+
const signature = JSON.stringify([candidates, manifestHref]);
|
|
2269
|
+
if (signature === this.iconSource) return;
|
|
2270
|
+
this.cancelIconCapture();
|
|
2271
|
+
this.iconSource = signature;
|
|
2272
|
+
const generation = this.iconGeneration;
|
|
2273
|
+
void (async () => {
|
|
2274
|
+
let source = candidates[0];
|
|
2275
|
+
let url = candidates.find((candidate) => /^https?:/.test(candidate));
|
|
2276
|
+
if ((!source || !url) && manifestHref && new URL(manifestHref).origin === window.location.origin) {
|
|
2277
|
+
const controller = new AbortController();
|
|
2278
|
+
let rejectPending = () => void 0;
|
|
2279
|
+
const timeout = new Promise((_, reject) => {
|
|
2280
|
+
rejectPending = reject;
|
|
185
2281
|
});
|
|
186
|
-
|
|
2282
|
+
const cleanup = () => {
|
|
2283
|
+
clearTimeout(timer);
|
|
2284
|
+
controller.abort();
|
|
2285
|
+
rejectPending(new Error("Icon capture cancelled"));
|
|
2286
|
+
};
|
|
2287
|
+
const timer = setTimeout(cleanup, 4e3);
|
|
2288
|
+
this.iconCleanups.add(cleanup);
|
|
2289
|
+
try {
|
|
2290
|
+
const data = await Promise.race([
|
|
2291
|
+
fetch(manifestHref, { signal: controller.signal, redirect: "error" }).then(
|
|
2292
|
+
(response) => {
|
|
2293
|
+
if (!response.ok) return void 0;
|
|
2294
|
+
return response.json();
|
|
2295
|
+
}
|
|
2296
|
+
),
|
|
2297
|
+
timeout
|
|
2298
|
+
]);
|
|
2299
|
+
{
|
|
2300
|
+
const icons = data && typeof data === "object" && "icons" in data ? data.icons : void 0;
|
|
2301
|
+
if (Array.isArray(icons)) {
|
|
2302
|
+
const ranked = icons.filter(
|
|
2303
|
+
(icon) => !!icon && typeof icon === "object" && typeof icon.src === "string"
|
|
2304
|
+
).sort(
|
|
2305
|
+
(a, b) => {
|
|
2306
|
+
var _a2, _b;
|
|
2307
|
+
return this.iconSize(
|
|
2308
|
+
typeof b.sizes === "string" ? b.sizes : "",
|
|
2309
|
+
(_a2 = b.type) != null ? _a2 : ""
|
|
2310
|
+
) - this.iconSize(
|
|
2311
|
+
typeof a.sizes === "string" ? a.sizes : "",
|
|
2312
|
+
(_b = a.type) != null ? _b : ""
|
|
2313
|
+
);
|
|
2314
|
+
}
|
|
2315
|
+
).flatMap((icon) => {
|
|
2316
|
+
const href = this.resolveIcon(icon.src, manifestHref);
|
|
2317
|
+
return href ? [href] : [];
|
|
2318
|
+
});
|
|
2319
|
+
source != null ? source : source = ranked[0];
|
|
2320
|
+
url != null ? url : url = ranked.find((href) => /^https?:/.test(href));
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
} catch {
|
|
2324
|
+
} finally {
|
|
2325
|
+
cleanup();
|
|
2326
|
+
this.iconCleanups.delete(cleanup);
|
|
2327
|
+
}
|
|
187
2328
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
2329
|
+
if (this.destroyed || generation !== this.iconGeneration) return;
|
|
2330
|
+
const fallback = url ? void 0 : this.iconFallback();
|
|
2331
|
+
url != null ? url : url = fallback;
|
|
2332
|
+
source != null ? source : source = url;
|
|
2333
|
+
const result = source ? await this.rasterizeIcon(source) : void 0;
|
|
2334
|
+
if (this.destroyed || generation !== this.iconGeneration) return;
|
|
2335
|
+
const manifest = this.loadManifest();
|
|
2336
|
+
if (!manifest) return;
|
|
2337
|
+
const isSameOrigin = (href) => href.startsWith(window.location.origin + "/");
|
|
2338
|
+
const broken = (result == null ? void 0 : result.status) !== "loaded" && (url === fallback || !!url && (result == null ? void 0 : result.status) === "failed" && isSameOrigin(url));
|
|
2339
|
+
manifest.suggestedIconUrl = broken ? void 0 : url;
|
|
2340
|
+
manifest.suggestedIconDataUrl = result == null ? void 0 : result.dataUrl;
|
|
2341
|
+
this.saveManifest(manifest);
|
|
2342
|
+
this.updateManifestHud(manifest);
|
|
2343
|
+
this.refreshPublishPromptLink(manifest);
|
|
2344
|
+
})().catch(() => void 0);
|
|
2345
|
+
}
|
|
2346
|
+
rasterizeIcon(source) {
|
|
2347
|
+
return new Promise((resolve) => {
|
|
2348
|
+
const image = new Image();
|
|
2349
|
+
const finish = (status, dataUrl) => {
|
|
2350
|
+
clearTimeout(timer);
|
|
2351
|
+
image.onload = null;
|
|
2352
|
+
image.onerror = null;
|
|
2353
|
+
this.iconCleanups.delete(cancel);
|
|
2354
|
+
resolve({ status, dataUrl });
|
|
2355
|
+
};
|
|
2356
|
+
const cancel = () => {
|
|
2357
|
+
finish("timeout");
|
|
2358
|
+
image.removeAttribute("src");
|
|
2359
|
+
};
|
|
2360
|
+
const timer = setTimeout(cancel, 4e3);
|
|
2361
|
+
this.iconCleanups.add(cancel);
|
|
2362
|
+
image.onerror = () => finish("failed");
|
|
2363
|
+
image.onload = () => {
|
|
2364
|
+
try {
|
|
2365
|
+
if (!image.naturalWidth || !image.naturalHeight) return finish("failed");
|
|
2366
|
+
const canvas = document.createElement("canvas");
|
|
2367
|
+
for (const size of [128, 96, 64]) {
|
|
2368
|
+
canvas.width = canvas.height = size;
|
|
2369
|
+
const context = canvas.getContext("2d");
|
|
2370
|
+
if (!context) break;
|
|
2371
|
+
const ratio = size / Math.max(image.naturalWidth, image.naturalHeight);
|
|
2372
|
+
const width = image.naturalWidth * ratio;
|
|
2373
|
+
const height = image.naturalHeight * ratio;
|
|
2374
|
+
context.drawImage(
|
|
2375
|
+
image,
|
|
2376
|
+
(size - width) / 2,
|
|
2377
|
+
(size - height) / 2,
|
|
2378
|
+
width,
|
|
2379
|
+
height
|
|
2380
|
+
);
|
|
2381
|
+
const data = canvas.toDataURL("image/webp", 0.85);
|
|
2382
|
+
if (data.startsWith("data:image/") && data.length <= 3e3)
|
|
2383
|
+
return finish("loaded", data);
|
|
2384
|
+
}
|
|
2385
|
+
} catch {
|
|
199
2386
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
2387
|
+
finish("loaded");
|
|
2388
|
+
};
|
|
2389
|
+
if (/^https?:/.test(source) && new URL(source).origin !== window.location.origin)
|
|
2390
|
+
image.crossOrigin = "anonymous";
|
|
2391
|
+
image.src = source;
|
|
2392
|
+
});
|
|
2393
|
+
}
|
|
2394
|
+
manifestStorageKey() {
|
|
2395
|
+
return `${this.options.namespace}:manifests`;
|
|
2396
|
+
}
|
|
2397
|
+
legacyManifestStorageKey() {
|
|
2398
|
+
return `${this.options.namespace}:manifest`;
|
|
2399
|
+
}
|
|
2400
|
+
publishDismissedStorageKey() {
|
|
2401
|
+
return `${this.options.namespace}:publish-dismissed-at:${this.getAppFingerprint()}`;
|
|
2402
|
+
}
|
|
2403
|
+
hudCollapsedStorageKey() {
|
|
2404
|
+
return `${this.options.namespace}:manifest-hud-collapsed:${this.getAppFingerprint()}`;
|
|
2405
|
+
}
|
|
2406
|
+
loadManifestMap() {
|
|
2407
|
+
if (this.options.persist && typeof localStorage !== "undefined") {
|
|
2408
|
+
try {
|
|
2409
|
+
const rawMap = localStorage.getItem(this.manifestStorageKey());
|
|
2410
|
+
const rawLegacy = localStorage.getItem(this.legacyManifestStorageKey());
|
|
2411
|
+
if (!rawMap && !rawLegacy && Object.keys(this.memoryManifests).length > 0) {
|
|
2412
|
+
return this.cloneManifestMap(this.memoryManifests);
|
|
2413
|
+
}
|
|
2414
|
+
const manifests = rawMap ? this.parseManifestMap(rawMap) : {};
|
|
2415
|
+
if (rawLegacy) {
|
|
2416
|
+
try {
|
|
2417
|
+
const legacyManifest = JSON.parse(rawLegacy);
|
|
2418
|
+
manifests[this.readLegacyManifestFingerprint(legacyManifest)] = this.cloneManifest(legacyManifest);
|
|
2419
|
+
localStorage.setItem(this.manifestStorageKey(), JSON.stringify(manifests));
|
|
2420
|
+
localStorage.removeItem(this.legacyManifestStorageKey());
|
|
2421
|
+
} catch {
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
this.replaceMemoryManifestMap(manifests);
|
|
2425
|
+
return this.cloneManifestMap(manifests);
|
|
2426
|
+
} catch {
|
|
209
2427
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
2428
|
+
}
|
|
2429
|
+
return this.cloneManifestMap(this.memoryManifests);
|
|
2430
|
+
}
|
|
2431
|
+
parseManifestMap(raw) {
|
|
2432
|
+
const parsed = JSON.parse(raw);
|
|
2433
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
2434
|
+
const manifests = {};
|
|
2435
|
+
for (const [fingerprint, manifest] of Object.entries(parsed)) {
|
|
2436
|
+
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) continue;
|
|
2437
|
+
manifests[fingerprint] = this.cloneManifest(manifest);
|
|
2438
|
+
}
|
|
2439
|
+
return manifests;
|
|
2440
|
+
}
|
|
2441
|
+
cloneManifestMap(manifests) {
|
|
2442
|
+
const clone = {};
|
|
2443
|
+
for (const [fingerprint, manifest] of Object.entries(manifests)) {
|
|
2444
|
+
clone[fingerprint] = this.cloneManifest(manifest);
|
|
2445
|
+
}
|
|
2446
|
+
return clone;
|
|
2447
|
+
}
|
|
2448
|
+
replaceMemoryManifestMap(manifests) {
|
|
2449
|
+
for (const key of Object.keys(this.memoryManifests)) delete this.memoryManifests[key];
|
|
2450
|
+
for (const [key, manifest] of Object.entries(manifests)) {
|
|
2451
|
+
this.memoryManifests[key] = this.cloneManifest(manifest);
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
loadManifest() {
|
|
2455
|
+
const manifests = this.loadManifestMap();
|
|
2456
|
+
const manifest = manifests[this.getAppFingerprint()];
|
|
2457
|
+
if (manifest && (!manifest.appKey || this.options.appId !== void 0 && manifest.appKey !== this.options.appId)) {
|
|
2458
|
+
manifest.appKey = this.getAppKey();
|
|
2459
|
+
this.saveManifest(manifest);
|
|
2460
|
+
}
|
|
2461
|
+
return manifest ? this.cloneManifest(manifest) : void 0;
|
|
2462
|
+
}
|
|
2463
|
+
saveManifest(manifest) {
|
|
2464
|
+
const manifests = this.loadManifestMap();
|
|
2465
|
+
manifests[this.getAppFingerprint()] = this.cloneManifest(manifest);
|
|
2466
|
+
if (this.options.persist && typeof localStorage !== "undefined") {
|
|
2467
|
+
try {
|
|
2468
|
+
localStorage.setItem(this.manifestStorageKey(), JSON.stringify(manifests));
|
|
2469
|
+
this.replaceMemoryManifestMap(manifests);
|
|
2470
|
+
return;
|
|
2471
|
+
} catch {
|
|
227
2472
|
}
|
|
228
|
-
case "GET_SYNC_STATUS":
|
|
229
|
-
this.toast({
|
|
230
|
-
icon: "\u{1F504}",
|
|
231
|
-
segments: [
|
|
232
|
-
"In LearnCard, this reports data sync progress. Mock reports ready."
|
|
233
|
-
]
|
|
234
|
-
});
|
|
235
|
-
return Promise.resolve({
|
|
236
|
-
status: "ready",
|
|
237
|
-
progress: {
|
|
238
|
-
totalCredentials: 0,
|
|
239
|
-
completedCredentials: 0,
|
|
240
|
-
failedCredentials: 0,
|
|
241
|
-
retryCount: 0
|
|
242
|
-
}
|
|
243
|
-
});
|
|
244
|
-
default:
|
|
245
|
-
this.toast({
|
|
246
|
-
icon: "\u2728",
|
|
247
|
-
segments: ["In LearnCard, this would run ", { b: action }, "."]
|
|
248
|
-
});
|
|
249
|
-
return Promise.resolve({});
|
|
250
2473
|
}
|
|
2474
|
+
this.replaceMemoryManifestMap(manifests);
|
|
2475
|
+
}
|
|
2476
|
+
loadPublishDismissedAt() {
|
|
2477
|
+
if (this.options.persist && typeof localStorage !== "undefined") {
|
|
2478
|
+
try {
|
|
2479
|
+
const raw = localStorage.getItem(this.publishDismissedStorageKey());
|
|
2480
|
+
if (raw) return raw;
|
|
2481
|
+
} catch {
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
return this.memoryPublishDismissedAt[this.getAppFingerprint()];
|
|
2485
|
+
}
|
|
2486
|
+
savePublishDismissedAt(value) {
|
|
2487
|
+
if (this.options.persist && typeof localStorage !== "undefined") {
|
|
2488
|
+
try {
|
|
2489
|
+
localStorage.setItem(this.publishDismissedStorageKey(), value);
|
|
2490
|
+
this.memoryPublishDismissedAt[this.getAppFingerprint()] = value;
|
|
2491
|
+
return;
|
|
2492
|
+
} catch {
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
this.memoryPublishDismissedAt[this.getAppFingerprint()] = value;
|
|
2496
|
+
}
|
|
2497
|
+
loadHudCollapsed() {
|
|
2498
|
+
if (this.options.persist && typeof localStorage !== "undefined") {
|
|
2499
|
+
try {
|
|
2500
|
+
const raw2 = localStorage.getItem(this.hudCollapsedStorageKey());
|
|
2501
|
+
if (raw2 === "true") return true;
|
|
2502
|
+
if (raw2 === "false") return false;
|
|
2503
|
+
} catch {
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
const raw = this.memoryHudCollapsed[this.getAppFingerprint()];
|
|
2507
|
+
if (raw === "true") return true;
|
|
2508
|
+
if (raw === "false") return false;
|
|
2509
|
+
return true;
|
|
2510
|
+
}
|
|
2511
|
+
saveHudCollapsed(value) {
|
|
2512
|
+
const serialized = value ? "true" : "false";
|
|
2513
|
+
if (this.options.persist && typeof localStorage !== "undefined") {
|
|
2514
|
+
try {
|
|
2515
|
+
localStorage.setItem(this.hudCollapsedStorageKey(), serialized);
|
|
2516
|
+
this.memoryHudCollapsed[this.getAppFingerprint()] = serialized;
|
|
2517
|
+
return;
|
|
2518
|
+
} catch {
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
this.memoryHudCollapsed[this.getAppFingerprint()] = serialized;
|
|
2522
|
+
}
|
|
2523
|
+
addPermission(manifest, permission) {
|
|
2524
|
+
this.addUnique(manifest.permissions, permission);
|
|
2525
|
+
}
|
|
2526
|
+
addUnique(values, value) {
|
|
2527
|
+
if (!values.includes(value)) values.push(value);
|
|
2528
|
+
}
|
|
2529
|
+
captureTemplateRecord(manifest, alias, template, version) {
|
|
2530
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2531
|
+
const existingIndex = manifest.templates.findIndex((record) => record.alias === alias);
|
|
2532
|
+
const nextRecord = {
|
|
2533
|
+
alias,
|
|
2534
|
+
template,
|
|
2535
|
+
version,
|
|
2536
|
+
lastUsedAt: now
|
|
2537
|
+
};
|
|
2538
|
+
if (existingIndex >= 0) {
|
|
2539
|
+
manifest.templates[existingIndex] = nextRecord;
|
|
2540
|
+
return;
|
|
2541
|
+
}
|
|
2542
|
+
manifest.templates.push(nextRecord);
|
|
2543
|
+
}
|
|
2544
|
+
captureConsentRecord(manifest, scopes) {
|
|
2545
|
+
const normalized = normalizeConsentRequest(scopes);
|
|
2546
|
+
const key = canonicalConsentScopeString(normalized);
|
|
2547
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2548
|
+
const existing = manifest.consentRequests.find(
|
|
2549
|
+
(record) => canonicalConsentScopeString(record.scopes) === key
|
|
2550
|
+
);
|
|
2551
|
+
if (existing) {
|
|
2552
|
+
existing.lastUsedAt = now;
|
|
2553
|
+
if (!existing.reason && typeof scopes.reason === "string" && scopes.reason) {
|
|
2554
|
+
existing.reason = scopes.reason;
|
|
2555
|
+
}
|
|
2556
|
+
return;
|
|
2557
|
+
}
|
|
2558
|
+
const nextRecord = {
|
|
2559
|
+
scopes: normalized,
|
|
2560
|
+
...typeof scopes.reason === "string" && scopes.reason ? { reason: scopes.reason } : {},
|
|
2561
|
+
lastUsedAt: now
|
|
2562
|
+
};
|
|
2563
|
+
manifest.consentRequests.push(nextRecord);
|
|
2564
|
+
}
|
|
2565
|
+
cloneManifest(manifest) {
|
|
2566
|
+
return JSON.parse(JSON.stringify(manifest));
|
|
2567
|
+
}
|
|
2568
|
+
isManifestPublishable(manifest) {
|
|
2569
|
+
return manifest.templates.length >= 1 || manifest.permissions.length >= 2;
|
|
2570
|
+
}
|
|
2571
|
+
/**
|
|
2572
|
+
* True when publish links would point at production LearnCard from a
|
|
2573
|
+
* machine that is almost certainly running LearnCard locally — the one
|
|
2574
|
+
* case where the developer probably wants the override.
|
|
2575
|
+
*/
|
|
2576
|
+
shouldHintLocalPublishOverride() {
|
|
2577
|
+
var _a;
|
|
2578
|
+
if (this.context.publishOriginPinned) return false;
|
|
2579
|
+
if (this.options.publishOrigin !== DEFAULT_PUBLISH_ORIGIN$1) return false;
|
|
2580
|
+
if (typeof window === "undefined") return false;
|
|
2581
|
+
const hostname = (_a = window.location) == null ? void 0 : _a.hostname;
|
|
2582
|
+
return hostname === "localhost" || hostname === "127.0.0.1";
|
|
2583
|
+
}
|
|
2584
|
+
getPublishUrl(manifest = this.loadManifest()) {
|
|
2585
|
+
if (!manifest) return void 0;
|
|
2586
|
+
const prefix = `${this.options.publishOrigin}/app-store/developer/submit?manifest=`;
|
|
2587
|
+
const url = prefix + encodeManifestForUrl(manifest);
|
|
2588
|
+
if (url.length <= 7e3) return url;
|
|
2589
|
+
const compact = { ...manifest };
|
|
2590
|
+
delete compact.suggestedIconDataUrl;
|
|
2591
|
+
return prefix + encodeManifestForUrl(compact);
|
|
2592
|
+
}
|
|
2593
|
+
maybeShowPublishPrompt(manifest) {
|
|
2594
|
+
if (!this.options.publishPrompt || !this.options.ui || !hasDocument()) return;
|
|
2595
|
+
if (this.publishPromptShown || !this.isManifestPublishable(manifest)) return;
|
|
2596
|
+
const dismissedAt = this.loadPublishDismissedAt();
|
|
2597
|
+
if (dismissedAt) {
|
|
2598
|
+
const dismissedMs = Date.parse(dismissedAt);
|
|
2599
|
+
if (Number.isFinite(dismissedMs) && Date.now() - dismissedMs < PUBLISH_DISMISS_TTL_MS) {
|
|
2600
|
+
return;
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
this.publishPromptShown = true;
|
|
2604
|
+
this.toast({
|
|
2605
|
+
icon: "publish",
|
|
2606
|
+
tone: "publish",
|
|
2607
|
+
persistent: true,
|
|
2608
|
+
dismissible: true,
|
|
2609
|
+
title: "Ready to publish",
|
|
2610
|
+
action: {
|
|
2611
|
+
label: "Publish to LearnCard",
|
|
2612
|
+
href: this.getPublishUrl(manifest)
|
|
2613
|
+
},
|
|
2614
|
+
segments: [
|
|
2615
|
+
"Your app works with LearnCard. Put it in front of learners in a few steps."
|
|
2616
|
+
]
|
|
2617
|
+
});
|
|
2618
|
+
}
|
|
2619
|
+
refreshPublishPromptLink(manifest) {
|
|
2620
|
+
var _a;
|
|
2621
|
+
const link = (_a = this.stackEl) == null ? void 0 : _a.querySelector(
|
|
2622
|
+
".lc-mock-toast--publish .lc-mock-action"
|
|
2623
|
+
);
|
|
2624
|
+
if (link) link.href = this.getPublishUrl(manifest);
|
|
2625
|
+
}
|
|
2626
|
+
describeFeatures(manifest) {
|
|
2627
|
+
const features = [];
|
|
2628
|
+
for (const permission of manifest.permissions) {
|
|
2629
|
+
const known = PERMISSION_FEATURES[permission];
|
|
2630
|
+
const feature = known ? { ...known } : { icon: "sparkles", label: permission.replace(/_/g, " ") };
|
|
2631
|
+
if (permission === "send_credential" && manifest.templates.length > 0) {
|
|
2632
|
+
feature.detail = manifest.templates.map(readableTemplateName).join(", ");
|
|
2633
|
+
}
|
|
2634
|
+
if (permission === "request_consent") {
|
|
2635
|
+
if (needsConsentSetup(manifest)) {
|
|
2636
|
+
feature.detail = "Not set up yet";
|
|
2637
|
+
feature.needsSetup = true;
|
|
2638
|
+
} else {
|
|
2639
|
+
feature.detail = this.formatConsentSummary(manifest);
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
if (permission === "launch_feature" && manifest.featuresLaunched.length > 0) {
|
|
2643
|
+
feature.detail = Array.from(
|
|
2644
|
+
new Set(manifest.featuresLaunched.map(readablePath))
|
|
2645
|
+
).join(", ");
|
|
2646
|
+
}
|
|
2647
|
+
features.push(feature);
|
|
2648
|
+
}
|
|
2649
|
+
if (manifest.templates.length > 0 && !manifest.permissions.includes("send_credential")) {
|
|
2650
|
+
features.push({
|
|
2651
|
+
icon: "award",
|
|
2652
|
+
label: "Award credentials",
|
|
2653
|
+
detail: manifest.templates.map(readableTemplateName).join(", ")
|
|
2654
|
+
});
|
|
2655
|
+
}
|
|
2656
|
+
if (manifest.usedLearnerContext) {
|
|
2657
|
+
features.push({ icon: "sparkles", label: "Personalize with the learner\u2019s profile" });
|
|
2658
|
+
}
|
|
2659
|
+
if (manifest.usedNotifications) {
|
|
2660
|
+
features.push({ icon: "bell", label: "Send notifications" });
|
|
2661
|
+
}
|
|
2662
|
+
if (manifest.counterKeys.length > 0) {
|
|
2663
|
+
features.push({
|
|
2664
|
+
icon: "hash",
|
|
2665
|
+
label: "Track progress",
|
|
2666
|
+
detail: manifest.counterKeys.join(", ")
|
|
2667
|
+
});
|
|
2668
|
+
}
|
|
2669
|
+
return features;
|
|
2670
|
+
}
|
|
2671
|
+
formatConsentSummary(manifest) {
|
|
2672
|
+
if (manifest.consentRequests.length === 0) return void 0;
|
|
2673
|
+
const readCategories = /* @__PURE__ */ new Set();
|
|
2674
|
+
const personalFields = /* @__PURE__ */ new Set();
|
|
2675
|
+
const writeCategories = /* @__PURE__ */ new Set();
|
|
2676
|
+
for (const request of manifest.consentRequests) {
|
|
2677
|
+
request.scopes.read.credentialCategories.forEach(
|
|
2678
|
+
(category) => readCategories.add(category)
|
|
2679
|
+
);
|
|
2680
|
+
request.scopes.read.personalFields.forEach((field) => personalFields.add(field));
|
|
2681
|
+
request.scopes.write.credentialCategories.forEach(
|
|
2682
|
+
(category) => writeCategories.add(category)
|
|
2683
|
+
);
|
|
2684
|
+
}
|
|
2685
|
+
const parts = [];
|
|
2686
|
+
if (readCategories.size > 0) parts.push(`See ${[...readCategories].join(", ")}`);
|
|
2687
|
+
if (personalFields.size > 0) parts.push(`See ${[...personalFields].join(", ")}`);
|
|
2688
|
+
if (writeCategories.size > 0) parts.push(`Add ${[...writeCategories].join(", ")}`);
|
|
2689
|
+
return parts.join(" \xB7 ") || void 0;
|
|
2690
|
+
}
|
|
2691
|
+
copyText(text) {
|
|
2692
|
+
var _a;
|
|
2693
|
+
if (typeof navigator !== "undefined" && ((_a = navigator.clipboard) == null ? void 0 : _a.writeText)) {
|
|
2694
|
+
navigator.clipboard.writeText(text).catch(() => void 0);
|
|
2695
|
+
return;
|
|
2696
|
+
}
|
|
2697
|
+
if (!hasDocument() || !document.body) return;
|
|
2698
|
+
const input = document.createElement("textarea");
|
|
2699
|
+
input.value = text;
|
|
2700
|
+
input.setAttribute("readonly", "true");
|
|
2701
|
+
Object.assign(input.style, {
|
|
2702
|
+
position: "fixed",
|
|
2703
|
+
opacity: "0",
|
|
2704
|
+
pointerEvents: "none"
|
|
2705
|
+
});
|
|
2706
|
+
document.body.appendChild(input);
|
|
2707
|
+
input.select();
|
|
2708
|
+
try {
|
|
2709
|
+
document.execCommand("copy");
|
|
2710
|
+
} catch {
|
|
2711
|
+
}
|
|
2712
|
+
input.remove();
|
|
251
2713
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
if (this.
|
|
262
|
-
|
|
263
|
-
|
|
2714
|
+
warnIfFingerprintCollides() {
|
|
2715
|
+
const manifest = this.loadManifest();
|
|
2716
|
+
const currentAppUrl = this.readAppOrigin();
|
|
2717
|
+
if (!manifest || !currentAppUrl || manifest.appUrl === currentAppUrl) return;
|
|
2718
|
+
console.warn(
|
|
2719
|
+
`${MOCK_PREFIX} Existing manifest state for fingerprint "${this.getAppFingerprint()}" belongs to ${manifest.appUrl}, but this page is ${currentAppUrl}. Two apps may be sharing mock state. Pass mockOptions.appId to isolate them.`
|
|
2720
|
+
);
|
|
2721
|
+
}
|
|
2722
|
+
updateManifestHud(manifest) {
|
|
2723
|
+
if (!this.options.ui || !hasDocument() || !document.body) return;
|
|
2724
|
+
const currentManifest = manifest != null ? manifest : this.loadManifest();
|
|
2725
|
+
if (!currentManifest) {
|
|
2726
|
+
if (this.hudEl) this.hudEl.style.display = "none";
|
|
2727
|
+
return;
|
|
264
2728
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
2729
|
+
this.ensureStyles();
|
|
2730
|
+
if (!this.hudEl || !document.body.contains(this.hudEl)) {
|
|
2731
|
+
const hud2 = createEl("div", "lc-mock-hud");
|
|
2732
|
+
hud2.setAttribute("role", "region");
|
|
2733
|
+
hud2.setAttribute("aria-label", "LearnCard practice mode");
|
|
2734
|
+
document.body.appendChild(hud2);
|
|
2735
|
+
this.domNodes.add(hud2);
|
|
2736
|
+
this.hudEl = hud2;
|
|
2737
|
+
this.hudAnimateNext = true;
|
|
268
2738
|
}
|
|
269
|
-
this.
|
|
270
|
-
|
|
2739
|
+
const hud = this.hudEl;
|
|
2740
|
+
hud.style.display = "";
|
|
2741
|
+
hud.replaceChildren(
|
|
2742
|
+
this.confirmingReset ? this.renderResetConfirmation() : this.loadHudCollapsed() ? this.renderHudPill(currentManifest) : this.renderHudCard(currentManifest)
|
|
2743
|
+
);
|
|
2744
|
+
hud.classList.toggle("lc-mock-animate", this.hudAnimateNext);
|
|
2745
|
+
this.hudAnimateNext = false;
|
|
271
2746
|
}
|
|
272
|
-
|
|
273
|
-
var _a
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
2747
|
+
setHudCollapsed(collapsed, manifest) {
|
|
2748
|
+
var _a;
|
|
2749
|
+
this.saveHudCollapsed(collapsed);
|
|
2750
|
+
this.hudAnimateNext = true;
|
|
2751
|
+
this.updateManifestHud(manifest);
|
|
2752
|
+
const focusTarget = (_a = this.hudEl) == null ? void 0 : _a.querySelector(
|
|
2753
|
+
collapsed ? ".lc-mock-hud-pill" : ".lc-mock-hud-minimize"
|
|
2754
|
+
);
|
|
2755
|
+
focusTarget == null ? void 0 : focusTarget.focus({ preventScroll: true });
|
|
2756
|
+
}
|
|
2757
|
+
renderHudPill(manifest) {
|
|
2758
|
+
const count = this.describeFeatures(manifest).length;
|
|
2759
|
+
const pill = createEl("button", "lc-mock-hud-pill lc-mock-glass");
|
|
2760
|
+
pill.type = "button";
|
|
2761
|
+
pill.setAttribute("aria-expanded", "false");
|
|
2762
|
+
pill.setAttribute(
|
|
2763
|
+
"aria-label",
|
|
2764
|
+
`LearnCard practice mode. Your app uses ${plural(count, "feature")}. Show details.`
|
|
2765
|
+
);
|
|
2766
|
+
const mark = createEl("span", "lc-mock-hud-mark");
|
|
2767
|
+
mark.appendChild(createIcon("mark"));
|
|
2768
|
+
const label = createEl("span", "lc-mock-hud-pill-label", "Practice mode");
|
|
2769
|
+
const badge = createEl("span", "lc-mock-hud-badge", String(count));
|
|
2770
|
+
pill.append(mark, createEl("span", "lc-mock-dot"), label, badge);
|
|
2771
|
+
pill.addEventListener("click", () => this.setHudCollapsed(false, manifest));
|
|
2772
|
+
return pill;
|
|
2773
|
+
}
|
|
2774
|
+
renderHudCard(manifest) {
|
|
2775
|
+
const card = createEl("div", "lc-mock-hud-card lc-mock-glass");
|
|
2776
|
+
const header = createEl("div", "lc-mock-hud-header");
|
|
2777
|
+
const mark = createEl("span", "lc-mock-hud-mark lc-mock-hud-mark--lg");
|
|
2778
|
+
mark.appendChild(createIcon("mark"));
|
|
2779
|
+
const heading = createEl("div", "lc-mock-hud-heading");
|
|
2780
|
+
const title = createEl("div", "lc-mock-hud-title");
|
|
2781
|
+
title.append(createEl("span", void 0, "Practice mode"), createEl("span", "lc-mock-dot"));
|
|
2782
|
+
heading.append(
|
|
2783
|
+
title,
|
|
2784
|
+
createEl("div", "lc-mock-hud-subtitle", "LearnCard actions are simulated here.")
|
|
2785
|
+
);
|
|
2786
|
+
const minimize = createEl("button", "lc-mock-hud-minimize");
|
|
2787
|
+
minimize.type = "button";
|
|
2788
|
+
minimize.setAttribute("aria-label", "Minimize");
|
|
2789
|
+
minimize.setAttribute("aria-expanded", "true");
|
|
2790
|
+
minimize.title = "Minimize";
|
|
2791
|
+
minimize.appendChild(createIcon("chevronDown"));
|
|
2792
|
+
minimize.addEventListener("click", () => this.setHudCollapsed(true, manifest));
|
|
2793
|
+
header.append(mark, heading, minimize);
|
|
2794
|
+
card.appendChild(header);
|
|
2795
|
+
card.appendChild(this.renderHudApp(manifest));
|
|
2796
|
+
const features = this.describeFeatures(manifest);
|
|
2797
|
+
card.appendChild(
|
|
2798
|
+
createEl(
|
|
2799
|
+
"div",
|
|
2800
|
+
"lc-mock-hud-section",
|
|
2801
|
+
features.length > 0 ? "What your app uses" : "Nothing yet"
|
|
2802
|
+
)
|
|
2803
|
+
);
|
|
2804
|
+
const list = createEl("ul", "lc-mock-hud-group lc-mock-hud-list");
|
|
2805
|
+
if (features.length === 0) {
|
|
2806
|
+
list.appendChild(
|
|
2807
|
+
createEl(
|
|
2808
|
+
"li",
|
|
2809
|
+
"lc-mock-hud-empty",
|
|
2810
|
+
"Use a LearnCard feature in your app and it will show up here."
|
|
2811
|
+
)
|
|
2812
|
+
);
|
|
2813
|
+
}
|
|
2814
|
+
for (const feature of features) {
|
|
2815
|
+
const row = createEl("li", "lc-mock-hud-row");
|
|
2816
|
+
const glyph = createEl("span", "lc-mock-hud-glyph");
|
|
2817
|
+
glyph.appendChild(createIcon(feature.icon));
|
|
2818
|
+
const text = createEl("div", "lc-mock-hud-row-text");
|
|
2819
|
+
text.appendChild(createEl("div", "lc-mock-hud-row-label", feature.label));
|
|
2820
|
+
if (feature.detail) {
|
|
2821
|
+
const detail = createEl(
|
|
2822
|
+
"div",
|
|
2823
|
+
`lc-mock-hud-row-detail${feature.needsSetup ? " is-setup" : ""}`,
|
|
2824
|
+
feature.detail
|
|
347
2825
|
);
|
|
2826
|
+
detail.title = feature.needsSetup ? "You can choose what to ask for when you publish." : feature.detail;
|
|
2827
|
+
text.appendChild(detail);
|
|
348
2828
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
value,
|
|
405
|
-
updatedAt: (_i = stored == null ? void 0 : stored.updatedAt) != null ? _i : null
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
case "get-counters": {
|
|
409
|
-
const requested = Array.isArray(event.keys) ? event.keys.map(String) : this.allCounterKeys();
|
|
410
|
-
const counters = requested.map((key) => {
|
|
411
|
-
var _a2, _b2;
|
|
412
|
-
const stored = this.readCounter(key);
|
|
413
|
-
return { key, value: (_a2 = stored == null ? void 0 : stored.value) != null ? _a2 : 0, updatedAt: (_b2 = stored == null ? void 0 : stored.updatedAt) != null ? _b2 : null };
|
|
414
|
-
});
|
|
415
|
-
this.toast({
|
|
416
|
-
icon: "\u{1F522}",
|
|
417
|
-
segments: ["Read ", { b: String(counters.length) }, " counter(s)."]
|
|
418
|
-
});
|
|
419
|
-
return Promise.resolve({ counters });
|
|
420
|
-
}
|
|
421
|
-
case "send-ai-session-credential": {
|
|
422
|
-
const sessionTitle = typeof event.sessionTitle === "string" && event.sessionTitle ? event.sessionTitle : "AI session";
|
|
423
|
-
const sessionBoostUri = this.nextUri("lc:mock:session-boost");
|
|
424
|
-
const record = this.addCredential({
|
|
425
|
-
name: sessionTitle,
|
|
426
|
-
boostUri: sessionBoostUri,
|
|
427
|
-
status: "claimed"
|
|
428
|
-
});
|
|
429
|
-
this.toast({
|
|
430
|
-
icon: "\u2705",
|
|
431
|
-
segments: [
|
|
432
|
-
"In LearnCard, an AI session credential would be saved: ",
|
|
433
|
-
{ b: sessionTitle }
|
|
434
|
-
]
|
|
435
|
-
});
|
|
436
|
-
const isNewTopic = this.aiTopic === null;
|
|
437
|
-
if (!this.aiTopic) {
|
|
438
|
-
this.aiTopic = {
|
|
439
|
-
topicUri: this.nextUri("lc:mock:topic"),
|
|
440
|
-
topicCredentialUri: this.nextUri("lc:mock:topic-credential")
|
|
441
|
-
};
|
|
442
|
-
}
|
|
443
|
-
return Promise.resolve({
|
|
444
|
-
topicUri: this.aiTopic.topicUri,
|
|
445
|
-
...isNewTopic ? { topicCredentialUri: this.aiTopic.topicCredentialUri } : {},
|
|
446
|
-
sessionCredentialUri: record.credentialUri,
|
|
447
|
-
sessionBoostUri,
|
|
448
|
-
isNewTopic
|
|
449
|
-
});
|
|
2829
|
+
const status = createEl(
|
|
2830
|
+
"span",
|
|
2831
|
+
feature.needsSetup ? "lc-mock-hud-setup-dot" : "lc-mock-hud-check"
|
|
2832
|
+
);
|
|
2833
|
+
if (feature.needsSetup) status.setAttribute("aria-label", "Needs setup");
|
|
2834
|
+
else status.appendChild(createIcon("check"));
|
|
2835
|
+
row.append(glyph, text, status);
|
|
2836
|
+
list.appendChild(row);
|
|
2837
|
+
}
|
|
2838
|
+
card.appendChild(list);
|
|
2839
|
+
card.appendChild(this.renderHudFooter(manifest));
|
|
2840
|
+
const reset = createEl("button", "lc-mock-hud-reset", "Start over");
|
|
2841
|
+
reset.type = "button";
|
|
2842
|
+
reset.addEventListener("click", () => {
|
|
2843
|
+
var _a, _b;
|
|
2844
|
+
this.confirmingReset = true;
|
|
2845
|
+
this.updateManifestHud();
|
|
2846
|
+
(_b = (_a = this.hudEl) == null ? void 0 : _a.querySelector(".lc-mock-hud-reset-cancel")) == null ? void 0 : _b.focus();
|
|
2847
|
+
});
|
|
2848
|
+
card.appendChild(reset);
|
|
2849
|
+
if (this.shouldHintLocalPublishOverride()) {
|
|
2850
|
+
const hint = createEl("div", "lc-mock-hud-publish-hint", "Local LearnCard? Add ");
|
|
2851
|
+
hint.appendChild(
|
|
2852
|
+
createEl("code", void 0, "?lc_publish_override=http://localhost:3000")
|
|
2853
|
+
);
|
|
2854
|
+
card.appendChild(hint);
|
|
2855
|
+
}
|
|
2856
|
+
return card;
|
|
2857
|
+
}
|
|
2858
|
+
renderResetConfirmation() {
|
|
2859
|
+
const card = createEl("div", "lc-mock-hud-card lc-mock-glass");
|
|
2860
|
+
const body = createEl("div", "lc-mock-hud-confirm");
|
|
2861
|
+
body.append(
|
|
2862
|
+
createEl("div", "lc-mock-hud-title", "Start over with this app?"),
|
|
2863
|
+
createEl(
|
|
2864
|
+
"p",
|
|
2865
|
+
void 0,
|
|
2866
|
+
"This clears what LearnCard recorded here \u2014 features, practice credentials, and counters. Your code isn't touched, and nothing already in LearnCard changes."
|
|
2867
|
+
)
|
|
2868
|
+
);
|
|
2869
|
+
const actions = createEl("div", "lc-mock-hud-confirm-actions");
|
|
2870
|
+
const cancel = createEl("button", "lc-mock-hud-reset-cancel", "Cancel");
|
|
2871
|
+
const confirm = createEl("button", "lc-mock-hud-reset-confirm", "Start Over");
|
|
2872
|
+
cancel.type = confirm.type = "button";
|
|
2873
|
+
const restore = () => {
|
|
2874
|
+
var _a, _b;
|
|
2875
|
+
this.confirmingReset = false;
|
|
2876
|
+
this.updateManifestHud();
|
|
2877
|
+
(_b = (_a = this.hudEl) == null ? void 0 : _a.querySelector(".lc-mock-hud-reset")) == null ? void 0 : _b.focus();
|
|
2878
|
+
};
|
|
2879
|
+
cancel.addEventListener("click", restore);
|
|
2880
|
+
card.addEventListener("keydown", (event) => {
|
|
2881
|
+
if (event.key === "Escape") {
|
|
2882
|
+
event.preventDefault();
|
|
2883
|
+
restore();
|
|
450
2884
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
2885
|
+
});
|
|
2886
|
+
confirm.addEventListener("click", () => this.resetPracticeMode());
|
|
2887
|
+
actions.append(cancel, confirm);
|
|
2888
|
+
body.appendChild(actions);
|
|
2889
|
+
card.appendChild(body);
|
|
2890
|
+
return card;
|
|
2891
|
+
}
|
|
2892
|
+
renderHudApp(manifest) {
|
|
2893
|
+
var _a;
|
|
2894
|
+
const app = createEl("div", "lc-mock-hud-group lc-mock-hud-app");
|
|
2895
|
+
const name = manifest.suggestedName || "Untitled app";
|
|
2896
|
+
const avatar = createEl("span", "lc-mock-hud-avatar");
|
|
2897
|
+
const initial = name.trim().charAt(0).toUpperCase() || "A";
|
|
2898
|
+
const icon = (_a = manifest.suggestedIconDataUrl) != null ? _a : manifest.suggestedIconUrl;
|
|
2899
|
+
if (icon) {
|
|
2900
|
+
const img = createEl("img");
|
|
2901
|
+
img.alt = "";
|
|
2902
|
+
img.src = icon;
|
|
2903
|
+
img.addEventListener("error", () => {
|
|
2904
|
+
img.remove();
|
|
2905
|
+
avatar.textContent = initial;
|
|
2906
|
+
});
|
|
2907
|
+
avatar.appendChild(img);
|
|
2908
|
+
} else {
|
|
2909
|
+
avatar.textContent = initial;
|
|
457
2910
|
}
|
|
2911
|
+
const text = createEl("div", "lc-mock-hud-row-text");
|
|
2912
|
+
text.appendChild(createEl("div", "lc-mock-hud-app-name", name));
|
|
2913
|
+
const host = readHost(manifest.appUrl);
|
|
2914
|
+
if (host) text.appendChild(createEl("div", "lc-mock-hud-row-detail", host));
|
|
2915
|
+
app.append(avatar, text);
|
|
2916
|
+
return app;
|
|
2917
|
+
}
|
|
2918
|
+
renderHudFooter(manifest) {
|
|
2919
|
+
const publishUrl = this.getPublishUrl(manifest);
|
|
2920
|
+
const footer = createEl("div", "lc-mock-hud-footer");
|
|
2921
|
+
const publish = createEl("a", "lc-mock-hud-publish");
|
|
2922
|
+
publish.href = publishUrl;
|
|
2923
|
+
publish.target = "_blank";
|
|
2924
|
+
publish.rel = "noopener";
|
|
2925
|
+
publish.append(createEl("span", void 0, "Publish app"), createIcon("open"));
|
|
2926
|
+
const copy = createEl("button", "lc-mock-hud-copy");
|
|
2927
|
+
copy.type = "button";
|
|
2928
|
+
copy.title = "Copy publish link";
|
|
2929
|
+
copy.setAttribute("aria-label", "Copy publish link");
|
|
2930
|
+
copy.appendChild(createIcon("copy"));
|
|
2931
|
+
copy.addEventListener("click", () => {
|
|
2932
|
+
this.copyText(publishUrl);
|
|
2933
|
+
copy.classList.add("is-copied");
|
|
2934
|
+
copy.replaceChildren(createIcon("check"));
|
|
2935
|
+
copy.title = "Link copied";
|
|
2936
|
+
copy.setAttribute("aria-label", "Link copied");
|
|
2937
|
+
const reset = setTimeout(() => {
|
|
2938
|
+
this.exitTimers.delete(reset);
|
|
2939
|
+
copy.classList.remove("is-copied");
|
|
2940
|
+
copy.replaceChildren(createIcon("copy"));
|
|
2941
|
+
copy.title = "Copy publish link";
|
|
2942
|
+
copy.setAttribute("aria-label", "Copy publish link");
|
|
2943
|
+
}, 1800);
|
|
2944
|
+
this.exitTimers.add(reset);
|
|
2945
|
+
});
|
|
2946
|
+
footer.append(publish, copy);
|
|
2947
|
+
return footer;
|
|
458
2948
|
}
|
|
459
2949
|
nextUri(prefix) {
|
|
460
2950
|
this.idSeq += 1;
|
|
@@ -509,7 +2999,7 @@ class MockHost {
|
|
|
509
2999
|
};
|
|
510
3000
|
}
|
|
511
3001
|
counterStorageKey() {
|
|
512
|
-
return `${this.options.namespace}:counters`;
|
|
3002
|
+
return `${this.options.namespace}:counters:${this.getAppFingerprint()}`;
|
|
513
3003
|
}
|
|
514
3004
|
loadCounters() {
|
|
515
3005
|
if (this.options.persist && typeof localStorage !== "undefined") {
|
|
@@ -553,24 +3043,45 @@ class MockHost {
|
|
|
553
3043
|
console.log(`${MOCK_PREFIX} ${action}`, payload != null ? payload : "");
|
|
554
3044
|
}
|
|
555
3045
|
announce() {
|
|
3046
|
+
this.warnIfFingerprintCollides();
|
|
3047
|
+
this.updateManifestHud();
|
|
556
3048
|
if (!this.options.log) return;
|
|
557
3049
|
console.log(
|
|
558
3050
|
`${MOCK_PREFIX} Standalone mock mode is active. The SDK is simulating the LearnCard host locally. When embedded in a real host, these calls run against it unchanged.`
|
|
559
3051
|
);
|
|
560
3052
|
}
|
|
561
|
-
showClaimToast(credentialName) {
|
|
3053
|
+
showClaimToast(credentialName, templateVersion) {
|
|
3054
|
+
const versionSuffix = typeof templateVersion === "number" && templateVersion > 1 ? ` Template updated to v${templateVersion}.` : "";
|
|
562
3055
|
this.toast({
|
|
563
|
-
icon: "
|
|
3056
|
+
icon: "award",
|
|
3057
|
+
tone: "positive",
|
|
564
3058
|
ttl: 5200,
|
|
565
|
-
segments: [
|
|
3059
|
+
segments: [
|
|
3060
|
+
"In LearnCard, the learner would receive ",
|
|
3061
|
+
{ b: credentialName },
|
|
3062
|
+
`.${versionSuffix}`
|
|
3063
|
+
]
|
|
566
3064
|
});
|
|
567
3065
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
3066
|
+
showConsentToast(payload, redirectIgnored) {
|
|
3067
|
+
const scopes = payload == null ? void 0 : payload.scopes;
|
|
3068
|
+
const contractUri = payload == null ? void 0 : payload.contractUri;
|
|
3069
|
+
if (!scopes && !contractUri) {
|
|
3070
|
+
this.toast({
|
|
3071
|
+
icon: "shield",
|
|
3072
|
+
tone: "warning",
|
|
3073
|
+
ttl: 8e3,
|
|
3074
|
+
title: "Choose what to ask for",
|
|
3075
|
+
segments: [
|
|
3076
|
+
"Approved here, but once published LearnCard needs to know what to ask the learner. You can choose it when you publish, or say it in code: ",
|
|
3077
|
+
{ b: "requestConsent({ read: { credentialCategories: ['Achievement'] } })" }
|
|
3078
|
+
]
|
|
3079
|
+
});
|
|
3080
|
+
} else {
|
|
3081
|
+
const segments = buildConsentScopeToastSegments(payload);
|
|
3082
|
+
if (redirectIgnored) segments.push(" Redirect skipped in practice.");
|
|
3083
|
+
this.toast({ icon: "shield", tone: "positive", segments });
|
|
3084
|
+
}
|
|
574
3085
|
if (redirectIgnored) {
|
|
575
3086
|
this.note(
|
|
576
3087
|
"requestConsent: 'redirect' is ignored in mock mode \u2014 the real host would navigate to the contract's redirectUrl with the VP in the URL."
|
|
@@ -587,61 +3098,115 @@ class MockHost {
|
|
|
587
3098
|
* calls never spam the screen.
|
|
588
3099
|
*/
|
|
589
3100
|
toast(spec) {
|
|
590
|
-
var _a, _b;
|
|
3101
|
+
var _a, _b, _c;
|
|
591
3102
|
if (!this.options.ui || !hasDocument()) return;
|
|
592
3103
|
const tone = (_a = spec.tone) != null ? _a : "default";
|
|
593
3104
|
const ttl = (_b = spec.ttl) != null ? _b : 4200;
|
|
3105
|
+
const persistent = Boolean(spec.persistent);
|
|
594
3106
|
const text = spec.segments.map((s) => typeof s === "string" ? s : s.b).join("");
|
|
595
|
-
const
|
|
3107
|
+
const actionKey = spec.action ? `|${spec.action.href}|${spec.action.label}` : "";
|
|
3108
|
+
const key = `${tone}|${spec.icon}|${(_c = spec.title) != null ? _c : ""}|${text}${actionKey}`;
|
|
596
3109
|
const existing = this.activeToasts.get(key);
|
|
597
3110
|
if (existing) {
|
|
598
3111
|
existing.count += 1;
|
|
599
3112
|
existing.countEl.textContent = `\xD7${existing.count}`;
|
|
600
3113
|
existing.countEl.style.display = "";
|
|
601
|
-
|
|
602
|
-
existing.
|
|
3114
|
+
existing.countEl.classList.remove("lc-mock-bump");
|
|
3115
|
+
void existing.countEl.offsetWidth;
|
|
3116
|
+
existing.countEl.classList.add("lc-mock-bump");
|
|
3117
|
+
this.scheduleToastDismiss(key);
|
|
603
3118
|
return;
|
|
604
3119
|
}
|
|
605
3120
|
const stack = this.ensureStack();
|
|
606
3121
|
if (!stack) return;
|
|
607
3122
|
this.ensureStyles();
|
|
608
|
-
const toast =
|
|
609
|
-
|
|
610
|
-
const
|
|
611
|
-
|
|
612
|
-
const
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
3123
|
+
const toast = createEl("div", `lc-mock-toast lc-mock-glass lc-mock-toast--${tone}`);
|
|
3124
|
+
if (spec.dismissible) toast.classList.add("lc-mock-toast--dismissible");
|
|
3125
|
+
const tile = createEl("span", "lc-mock-tile");
|
|
3126
|
+
tile.appendChild(createIcon(spec.icon));
|
|
3127
|
+
const content = createEl("div", "lc-mock-content");
|
|
3128
|
+
const meta = createEl("div", "lc-mock-meta");
|
|
3129
|
+
const countEl = createEl("span", "lc-mock-count");
|
|
3130
|
+
countEl.style.display = "none";
|
|
3131
|
+
meta.append(
|
|
3132
|
+
createEl("span", "lc-mock-meta-name", "LearnCard"),
|
|
3133
|
+
createEl("span", "lc-mock-chip", "Practice"),
|
|
3134
|
+
countEl
|
|
3135
|
+
);
|
|
3136
|
+
content.appendChild(meta);
|
|
3137
|
+
if (spec.title) content.appendChild(createEl("div", "lc-mock-title", spec.title));
|
|
3138
|
+
const body = createEl("div", "lc-mock-body");
|
|
621
3139
|
for (const seg of spec.segments) {
|
|
622
3140
|
if (typeof seg === "string") {
|
|
623
3141
|
body.append(seg);
|
|
624
3142
|
} else {
|
|
625
|
-
|
|
626
|
-
strong.textContent = seg.b;
|
|
627
|
-
body.appendChild(strong);
|
|
3143
|
+
body.appendChild(createEl("strong", void 0, seg.b));
|
|
628
3144
|
}
|
|
629
3145
|
}
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
3146
|
+
content.appendChild(body);
|
|
3147
|
+
if (spec.action) {
|
|
3148
|
+
const action = createEl("a", "lc-mock-action");
|
|
3149
|
+
action.href = spec.action.href;
|
|
3150
|
+
action.target = "_blank";
|
|
3151
|
+
action.rel = "noopener";
|
|
3152
|
+
action.append(createEl("span", void 0, spec.action.label), createIcon("open"));
|
|
3153
|
+
content.appendChild(action);
|
|
3154
|
+
}
|
|
3155
|
+
toast.append(tile, content);
|
|
3156
|
+
if (spec.dismissible) {
|
|
3157
|
+
const close = createEl("button", "lc-mock-close");
|
|
3158
|
+
close.type = "button";
|
|
3159
|
+
close.setAttribute("aria-label", "Dismiss");
|
|
3160
|
+
close.appendChild(createIcon("close"));
|
|
3161
|
+
close.addEventListener("click", () => {
|
|
3162
|
+
if (tone === "publish") {
|
|
3163
|
+
this.savePublishDismissedAt((/* @__PURE__ */ new Date()).toISOString());
|
|
3164
|
+
}
|
|
3165
|
+
this.dismissToast(key);
|
|
3166
|
+
});
|
|
3167
|
+
toast.appendChild(close);
|
|
3168
|
+
}
|
|
3169
|
+
toast.addEventListener("mouseenter", () => {
|
|
3170
|
+
const entry = this.activeToasts.get(key);
|
|
3171
|
+
if (entry == null ? void 0 : entry.timeoutId) {
|
|
3172
|
+
clearTimeout(entry.timeoutId);
|
|
3173
|
+
entry.timeoutId = null;
|
|
3174
|
+
}
|
|
3175
|
+
});
|
|
3176
|
+
toast.addEventListener("mouseleave", () => this.scheduleToastDismiss(key));
|
|
635
3177
|
stack.appendChild(toast);
|
|
636
3178
|
this.domNodes.add(toast);
|
|
637
|
-
|
|
638
|
-
|
|
3179
|
+
this.activeToasts.set(key, {
|
|
3180
|
+
node: toast,
|
|
3181
|
+
timeoutId: null,
|
|
3182
|
+
count: 1,
|
|
3183
|
+
countEl,
|
|
3184
|
+
ttl,
|
|
3185
|
+
persistent
|
|
3186
|
+
});
|
|
3187
|
+
this.scheduleToastDismiss(key);
|
|
3188
|
+
this.refreshToastVisibility();
|
|
3189
|
+
}
|
|
3190
|
+
scheduleToastDismiss(key) {
|
|
3191
|
+
const entry = this.activeToasts.get(key);
|
|
3192
|
+
if (!entry) return;
|
|
3193
|
+
if (entry.timeoutId) clearTimeout(entry.timeoutId);
|
|
3194
|
+
entry.timeoutId = entry.persistent ? null : setTimeout(() => this.dismissToast(key), entry.ttl);
|
|
3195
|
+
}
|
|
3196
|
+
refreshToastVisibility() {
|
|
3197
|
+
const transient = Array.from(this.activeToasts.values()).filter((entry) => !entry.persistent);
|
|
3198
|
+
const hiddenCount = Math.max(0, transient.length - MAX_VISIBLE_TOASTS);
|
|
3199
|
+
transient.forEach((entry, index) => {
|
|
3200
|
+
entry.node.classList.toggle("lc-mock-toast--hidden", index < hiddenCount);
|
|
3201
|
+
entry.node.classList.toggle("lc-mock-toast--older", index < transient.length - 1);
|
|
3202
|
+
});
|
|
639
3203
|
}
|
|
640
3204
|
dismissToast(key) {
|
|
641
3205
|
const entry = this.activeToasts.get(key);
|
|
642
3206
|
if (!entry) return;
|
|
643
3207
|
this.activeToasts.delete(key);
|
|
644
|
-
clearTimeout(entry.timeoutId);
|
|
3208
|
+
if (entry.timeoutId) clearTimeout(entry.timeoutId);
|
|
3209
|
+
this.refreshToastVisibility();
|
|
645
3210
|
const { node } = entry;
|
|
646
3211
|
node.classList.add("lc-mock-out");
|
|
647
3212
|
const exitTimer = setTimeout(() => {
|
|
@@ -654,8 +3219,9 @@ class MockHost {
|
|
|
654
3219
|
ensureStack() {
|
|
655
3220
|
if (!document.body) return null;
|
|
656
3221
|
if (this.stackEl && document.body.contains(this.stackEl)) return this.stackEl;
|
|
657
|
-
const stack =
|
|
658
|
-
stack.
|
|
3222
|
+
const stack = createEl("div", "lc-mock-stack");
|
|
3223
|
+
stack.setAttribute("role", "status");
|
|
3224
|
+
stack.setAttribute("aria-live", "polite");
|
|
659
3225
|
document.body.appendChild(stack);
|
|
660
3226
|
this.stackEl = stack;
|
|
661
3227
|
return stack;
|
|
@@ -663,32 +3229,7 @@ class MockHost {
|
|
|
663
3229
|
ensureStyles() {
|
|
664
3230
|
if (!this.options.ui || !hasDocument() || this.styleEl || !document.head) return;
|
|
665
3231
|
const style = document.createElement("style");
|
|
666
|
-
style.textContent =
|
|
667
|
-
@keyframes lc-mock-in { from { opacity: 0; transform: translateY(10px) scale(0.98); } to { opacity: 1; transform: none; } }
|
|
668
|
-
@keyframes lc-mock-out { to { opacity: 0; transform: translateY(6px); } }
|
|
669
|
-
.lc-mock-stack {
|
|
670
|
-
position: fixed; bottom: 20px; right: 20px; z-index: 2147483647;
|
|
671
|
-
display: flex; flex-direction: column; gap: 10px; align-items: flex-end;
|
|
672
|
-
pointer-events: none; max-width: min(360px, calc(100vw - 40px));
|
|
673
|
-
}
|
|
674
|
-
.lc-mock-toast {
|
|
675
|
-
pointer-events: auto; width: 100%; box-sizing: border-box; padding: 11px 14px; border-radius: 14px;
|
|
676
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 13.5px;
|
|
677
|
-
line-height: 1.45; box-shadow: 0 10px 30px rgba(24,34,78,0.22); animation: lc-mock-in 180ms cubic-bezier(0.2,0.8,0.2,1);
|
|
678
|
-
}
|
|
679
|
-
.lc-mock-toast.lc-mock-out { animation: lc-mock-out 180ms ease-in forwards; }
|
|
680
|
-
.lc-mock-toast--default { background: #18224E; color: #fff; }
|
|
681
|
-
.lc-mock-toast--positive { background: #ECFDF5; color: #065F46; border: 1px solid #A7F3D0; }
|
|
682
|
-
.lc-mock-badge {
|
|
683
|
-
display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 5px;
|
|
684
|
-
font-size: 11px; letter-spacing: 0.02em; text-transform: uppercase; opacity: 0.72;
|
|
685
|
-
}
|
|
686
|
-
.lc-mock-badge-name { font-weight: 600; }
|
|
687
|
-
.lc-mock-pill { font-size: 10px; font-weight: 700; padding: 2px 6px; border-radius: 999px; background: rgba(255,255,255,0.16); }
|
|
688
|
-
.lc-mock-toast--positive .lc-mock-pill { background: rgba(6,95,70,0.12); }
|
|
689
|
-
.lc-mock-body strong { font-weight: 700; }
|
|
690
|
-
.lc-mock-count { margin-left: 6px; font-weight: 700; opacity: 0.75; }
|
|
691
|
-
`.trim();
|
|
3232
|
+
style.textContent = MOCK_STYLES;
|
|
692
3233
|
document.head.appendChild(style);
|
|
693
3234
|
this.styleEl = style;
|
|
694
3235
|
}
|
|
@@ -697,8 +3238,45 @@ class MockHost {
|
|
|
697
3238
|
var __defProp = Object.defineProperty;
|
|
698
3239
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
699
3240
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
3241
|
+
const DEFAULT_PUBLISH_ORIGIN = "https://learncard.app";
|
|
700
3242
|
const SYNC_STATUS_POLL_MAX_DURATION_MS = 10 * 60 * 1e3;
|
|
701
3243
|
const DEFAULT_HOST_PROBE_TIMEOUT_MS = 1500;
|
|
3244
|
+
const joinInlineTemplateErrors = (errors) => errors.map((error) => `${error.path}: ${error.message}`).join("\n");
|
|
3245
|
+
const parseCompiledTemplateObject = (credentialTemplateJson) => {
|
|
3246
|
+
return JSON.parse(
|
|
3247
|
+
credentialTemplateJson.replace(/:(\s*)(\{\{\w+\}\})(?=\s*[,}\]])/g, ':$1"$2"')
|
|
3248
|
+
);
|
|
3249
|
+
};
|
|
3250
|
+
const previewCompiledTemplate = (template, templateData) => {
|
|
3251
|
+
const templateErrors = validateInlineTemplate(template);
|
|
3252
|
+
if (templateErrors.length > 0) {
|
|
3253
|
+
return { valid: false, errors: templateErrors };
|
|
3254
|
+
}
|
|
3255
|
+
const compiledTemplate = compileInlineTemplate(template);
|
|
3256
|
+
const compiled = parseCompiledTemplateObject(compiledTemplate.credentialTemplateJson);
|
|
3257
|
+
if (templateData === void 0) {
|
|
3258
|
+
return { valid: true, errors: [], compiled };
|
|
3259
|
+
}
|
|
3260
|
+
const dataErrors = validateTemplateData(compiledTemplate.variableManifest, templateData);
|
|
3261
|
+
if (dataErrors.length > 0) {
|
|
3262
|
+
return { valid: false, errors: dataErrors, compiled };
|
|
3263
|
+
}
|
|
3264
|
+
return {
|
|
3265
|
+
valid: true,
|
|
3266
|
+
errors: [],
|
|
3267
|
+
compiled,
|
|
3268
|
+
rendered: renderCompiledTemplate(compiledTemplate.credentialTemplateJson, templateData)
|
|
3269
|
+
};
|
|
3270
|
+
};
|
|
3271
|
+
const isTemplateAliasInput = (input) => Boolean(
|
|
3272
|
+
input && typeof input === "object" && "templateAlias" in input && typeof input.templateAlias === "string"
|
|
3273
|
+
);
|
|
3274
|
+
const isInlineTemplateInput = (input) => Boolean(
|
|
3275
|
+
input && typeof input === "object" && "alias" in input && typeof input.alias === "string" && "template" in input
|
|
3276
|
+
);
|
|
3277
|
+
const isConsentRequestInput = (input) => {
|
|
3278
|
+
return Boolean(input && typeof input === "object" && !Array.isArray(input));
|
|
3279
|
+
};
|
|
702
3280
|
const isLocalDevHost = (hostname) => hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "::1" || hostname.endsWith(".localhost") || hostname.endsWith(".local");
|
|
703
3281
|
function isEmbedded() {
|
|
704
3282
|
if (typeof window === "undefined") return false;
|
|
@@ -721,6 +3299,14 @@ const _PartnerConnect = class _PartnerConnect {
|
|
|
721
3299
|
__publicField(this, "syncCompleteCallbacks", /* @__PURE__ */ new Set());
|
|
722
3300
|
__publicField(this, "syncStatusPollId", null);
|
|
723
3301
|
__publicField(this, "mockHost", null);
|
|
3302
|
+
/** Internal, non-public context handed to every {@link MockHost} we build. */
|
|
3303
|
+
__publicField(this, "mockHostContext", {});
|
|
3304
|
+
/**
|
|
3305
|
+
* Memoized override-derived publish origin. `undefined` means "not looked
|
|
3306
|
+
* up yet"; `null` means "looked up, no usable override". Caching keeps the
|
|
3307
|
+
* warning for an invalid value to a single log per instance.
|
|
3308
|
+
*/
|
|
3309
|
+
__publicField(this, "pinnedPublishOrigin");
|
|
724
3310
|
__publicField(this, "embedded", false);
|
|
725
3311
|
__publicField(this, "warnedNoHost", false);
|
|
726
3312
|
__publicField(this, "hostProbeTimeout", DEFAULT_HOST_PROBE_TIMEOUT_MS);
|
|
@@ -744,6 +3330,112 @@ const _PartnerConnect = class _PartnerConnect {
|
|
|
744
3330
|
this.embedded = isEmbedded();
|
|
745
3331
|
this.configureMockActivation(options);
|
|
746
3332
|
}
|
|
3333
|
+
resolveMockOptions(options) {
|
|
3334
|
+
var _a, _b;
|
|
3335
|
+
if ((_a = options == null ? void 0 : options.mockOptions) == null ? void 0 : _a.publishOrigin) {
|
|
3336
|
+
this.mockHostContext = { publishOriginPinned: true };
|
|
3337
|
+
return options.mockOptions;
|
|
3338
|
+
}
|
|
3339
|
+
const publishOrigin = this.resolvePublishOrigin(options == null ? void 0 : options.hostOrigin);
|
|
3340
|
+
this.mockHostContext = { publishOriginPinned: this.readPinnedPublishOrigin() !== null };
|
|
3341
|
+
return { ...(_b = options == null ? void 0 : options.mockOptions) != null ? _b : {}, publishOrigin };
|
|
3342
|
+
}
|
|
3343
|
+
/**
|
|
3344
|
+
* Resolve the LearnCard origin that App Store publish links point at:
|
|
3345
|
+
* 1. `?lc_publish_override=` (query param, then this tab's stored value).
|
|
3346
|
+
* 2. `?lc_host_override=` (query param, then stored) when it names one
|
|
3347
|
+
* concrete http(s) origin — testing against a host implies publishing
|
|
3348
|
+
* to it.
|
|
3349
|
+
* 3. The first concrete configured `hostOrigin`.
|
|
3350
|
+
* 4. `DEFAULT_PUBLISH_ORIGIN`.
|
|
3351
|
+
*
|
|
3352
|
+
* `mockOptions.publishOrigin` outranks all of these and is applied by
|
|
3353
|
+
* {@link resolveMockOptions} before this runs.
|
|
3354
|
+
*/
|
|
3355
|
+
resolvePublishOrigin(hostOrigin) {
|
|
3356
|
+
var _a;
|
|
3357
|
+
return (_a = this.readPinnedPublishOrigin()) != null ? _a : this.inferPublishOrigin(hostOrigin);
|
|
3358
|
+
}
|
|
3359
|
+
readPinnedPublishOrigin() {
|
|
3360
|
+
var _a;
|
|
3361
|
+
if (this.pinnedPublishOrigin !== void 0) return this.pinnedPublishOrigin;
|
|
3362
|
+
this.pinnedPublishOrigin = (_a = this.readPublishOverride()) != null ? _a : this.readHostOverrideForPublish();
|
|
3363
|
+
return this.pinnedPublishOrigin;
|
|
3364
|
+
}
|
|
3365
|
+
/**
|
|
3366
|
+
* Unlike `lc_host_override`, this is not a security boundary — it only
|
|
3367
|
+
* decides which LearnCard origin publish links target — so any parseable
|
|
3368
|
+
* http(s) origin is accepted rather than being whitelist-checked.
|
|
3369
|
+
*/
|
|
3370
|
+
readPublishOverride() {
|
|
3371
|
+
if (typeof window === "undefined") return null;
|
|
3372
|
+
try {
|
|
3373
|
+
const param = new URLSearchParams(window.location.search).get("lc_publish_override");
|
|
3374
|
+
if (param) {
|
|
3375
|
+
const normalized = _PartnerConnect.normalizePublishOrigin(param);
|
|
3376
|
+
if (normalized) {
|
|
3377
|
+
this.persistPublishOverride(normalized);
|
|
3378
|
+
console.log("[LearnCard SDK] Using lc_publish_override:", normalized);
|
|
3379
|
+
return normalized;
|
|
3380
|
+
}
|
|
3381
|
+
console.warn(
|
|
3382
|
+
"[LearnCard SDK] lc_publish_override is not a valid http(s) origin; ignoring:",
|
|
3383
|
+
param
|
|
3384
|
+
);
|
|
3385
|
+
}
|
|
3386
|
+
const stored = _PartnerConnect.readSessionValue(
|
|
3387
|
+
_PartnerConnect.PUBLISH_SESSION_STORAGE_KEY
|
|
3388
|
+
);
|
|
3389
|
+
if (stored) return _PartnerConnect.normalizePublishOrigin(stored);
|
|
3390
|
+
} catch (error) {
|
|
3391
|
+
console.error("[LearnCard SDK] Error reading lc_publish_override:", error);
|
|
3392
|
+
}
|
|
3393
|
+
return null;
|
|
3394
|
+
}
|
|
3395
|
+
readHostOverrideForPublish() {
|
|
3396
|
+
if (typeof window === "undefined") return null;
|
|
3397
|
+
try {
|
|
3398
|
+
const candidates = [
|
|
3399
|
+
new URLSearchParams(window.location.search).get("lc_host_override"),
|
|
3400
|
+
_PartnerConnect.readSessionValue(_PartnerConnect.SESSION_STORAGE_KEY)
|
|
3401
|
+
];
|
|
3402
|
+
for (const candidate of candidates) {
|
|
3403
|
+
if (!candidate || candidate.includes("*")) continue;
|
|
3404
|
+
const normalized = _PartnerConnect.normalizePublishOrigin(candidate);
|
|
3405
|
+
if (normalized) return normalized;
|
|
3406
|
+
}
|
|
3407
|
+
} catch {
|
|
3408
|
+
}
|
|
3409
|
+
return null;
|
|
3410
|
+
}
|
|
3411
|
+
static normalizePublishOrigin(value) {
|
|
3412
|
+
try {
|
|
3413
|
+
const url = new URL(value);
|
|
3414
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
3415
|
+
return url.origin;
|
|
3416
|
+
} catch {
|
|
3417
|
+
return null;
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
3420
|
+
static readSessionValue(key) {
|
|
3421
|
+
try {
|
|
3422
|
+
return sessionStorage.getItem(key);
|
|
3423
|
+
} catch {
|
|
3424
|
+
return null;
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3427
|
+
inferPublishOrigin(hostOrigin) {
|
|
3428
|
+
const configuredOrigins = Array.isArray(hostOrigin) ? hostOrigin : hostOrigin ? [hostOrigin] : [];
|
|
3429
|
+
for (const origin of configuredOrigins) {
|
|
3430
|
+
if (origin.includes("*")) continue;
|
|
3431
|
+
if (this.isOriginNativeApp(origin)) continue;
|
|
3432
|
+
try {
|
|
3433
|
+
return new URL(origin).origin;
|
|
3434
|
+
} catch {
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
return DEFAULT_PUBLISH_ORIGIN;
|
|
3438
|
+
}
|
|
747
3439
|
/**
|
|
748
3440
|
* Decide whether this instance talks to a real host, simulates one, or
|
|
749
3441
|
* fails fast — the resolution of the `mock` option against the runtime
|
|
@@ -752,14 +3444,15 @@ const _PartnerConnect = class _PartnerConnect {
|
|
|
752
3444
|
configureMockActivation(options) {
|
|
753
3445
|
var _a;
|
|
754
3446
|
const mockSetting = (_a = options == null ? void 0 : options.mock) != null ? _a : "auto";
|
|
3447
|
+
const mockOptions = this.resolveMockOptions(options);
|
|
755
3448
|
if (mockSetting === true) {
|
|
756
|
-
this.mockHost = new MockHost(
|
|
3449
|
+
this.mockHost = new MockHost(mockOptions, this.mockHostContext);
|
|
757
3450
|
return;
|
|
758
3451
|
}
|
|
759
|
-
const canAutoMock = mockSetting === "standalone" || mockSetting === "auto" && this.isLocalDevContext();
|
|
3452
|
+
const canAutoMock = mockSetting === "standalone" || mockSetting === "auto" && (this.isLocalDevContext() || this.isAppBuilderContext());
|
|
760
3453
|
if (!this.embedded) {
|
|
761
3454
|
if (canAutoMock) {
|
|
762
|
-
this.mockHost = new MockHost(
|
|
3455
|
+
this.mockHost = new MockHost(mockOptions, this.mockHostContext);
|
|
763
3456
|
}
|
|
764
3457
|
return;
|
|
765
3458
|
}
|
|
@@ -769,13 +3462,13 @@ const _PartnerConnect = class _PartnerConnect {
|
|
|
769
3462
|
return;
|
|
770
3463
|
case "foreign":
|
|
771
3464
|
if (canAutoMock) {
|
|
772
|
-
this.mockHost = new MockHost(
|
|
3465
|
+
this.mockHost = new MockHost(mockOptions, this.mockHostContext);
|
|
773
3466
|
}
|
|
774
3467
|
return;
|
|
775
3468
|
case "ambiguous":
|
|
776
3469
|
if (canAutoMock) {
|
|
777
3470
|
this.hostReachable = true;
|
|
778
|
-
this.activation = this.probeHost(
|
|
3471
|
+
this.activation = this.probeHost(mockOptions);
|
|
779
3472
|
} else {
|
|
780
3473
|
this.hostReachable = true;
|
|
781
3474
|
}
|
|
@@ -786,6 +3479,28 @@ const _PartnerConnect = class _PartnerConnect {
|
|
|
786
3479
|
if (typeof window === "undefined") return false;
|
|
787
3480
|
return isLocalDevHost(window.location.hostname);
|
|
788
3481
|
}
|
|
3482
|
+
/**
|
|
3483
|
+
* Whether the app is running inside an AI app builder's preview (Lovable,
|
|
3484
|
+
* Bolt, v0, Replit): either on a preview-only host, or framed by the
|
|
3485
|
+
* builder's editor. Firefox hides ancestor origins, so the referrer of an
|
|
3486
|
+
* embedded document stands in for the parent origin there.
|
|
3487
|
+
*/
|
|
3488
|
+
isAppBuilderContext() {
|
|
3489
|
+
var _a;
|
|
3490
|
+
if (typeof window === "undefined") return false;
|
|
3491
|
+
if (isAppBuilderPreviewHost(window.location.hostname)) return true;
|
|
3492
|
+
if (!this.embedded) return false;
|
|
3493
|
+
const parentOrigin = (_a = this.readAncestorOrigin()) != null ? _a : this.readReferrerOrigin();
|
|
3494
|
+
return parentOrigin !== null && isAppBuilderEditorOrigin(parentOrigin);
|
|
3495
|
+
}
|
|
3496
|
+
readReferrerOrigin() {
|
|
3497
|
+
if (typeof document === "undefined" || !document.referrer) return null;
|
|
3498
|
+
try {
|
|
3499
|
+
return new URL(document.referrer).origin;
|
|
3500
|
+
} catch {
|
|
3501
|
+
return null;
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
789
3504
|
classifyParent() {
|
|
790
3505
|
const ancestorOrigin = this.readAncestorOrigin();
|
|
791
3506
|
if (!ancestorOrigin) return "ambiguous";
|
|
@@ -805,14 +3520,14 @@ const _PartnerConnect = class _PartnerConnect {
|
|
|
805
3520
|
* listening and the mock takes over. Requests issued while the probe is
|
|
806
3521
|
* in flight queue behind the decision instead of racing it.
|
|
807
3522
|
*/
|
|
808
|
-
probeHost(
|
|
3523
|
+
probeHost(mockOptions) {
|
|
809
3524
|
return this.postToHost("GET_SYNC_STATUS", void 0, this.hostProbeTimeout).then(() => {
|
|
810
3525
|
this.activation = null;
|
|
811
3526
|
}).catch(() => {
|
|
812
3527
|
this.activation = null;
|
|
813
3528
|
if (this.isInitialized) {
|
|
814
3529
|
this.hostReachable = false;
|
|
815
|
-
this.mockHost = new MockHost(
|
|
3530
|
+
this.mockHost = new MockHost(mockOptions, this.mockHostContext);
|
|
816
3531
|
console.warn(
|
|
817
3532
|
`[LearnCard SDK] Embedded in a frame, but no LearnCard host answered within ${this.hostProbeTimeout}ms \u2014 activating standalone mock mode. If a real local host was just slow to boot, raise \`hostProbeTimeout\`.`
|
|
818
3533
|
);
|
|
@@ -840,6 +3555,68 @@ const _PartnerConnect = class _PartnerConnect {
|
|
|
840
3555
|
isMocked() {
|
|
841
3556
|
return this.mockHost !== null;
|
|
842
3557
|
}
|
|
3558
|
+
/**
|
|
3559
|
+
* Read the app manifest captured by standalone mock mode.
|
|
3560
|
+
*
|
|
3561
|
+
* Returns `undefined` when this instance is talking to a real host, or when
|
|
3562
|
+
* mock mode has not captured anything yet.
|
|
3563
|
+
*
|
|
3564
|
+
* @example
|
|
3565
|
+
* ```typescript
|
|
3566
|
+
* const manifest = learnCard.getCapturedManifest();
|
|
3567
|
+
* if (manifest) {
|
|
3568
|
+
* console.log(manifest.permissions, manifest.templates);
|
|
3569
|
+
* }
|
|
3570
|
+
* ```
|
|
3571
|
+
*/
|
|
3572
|
+
getCapturedManifest() {
|
|
3573
|
+
var _a;
|
|
3574
|
+
return (_a = this.mockHost) == null ? void 0 : _a.getCapturedManifest();
|
|
3575
|
+
}
|
|
3576
|
+
/**
|
|
3577
|
+
* Build the LearnCard App Store submission URL for the current captured manifest.
|
|
3578
|
+
*
|
|
3579
|
+
* Returns `undefined` unless mock mode is active and the SDK has captured at
|
|
3580
|
+
* least one manifest interaction. The publish prompt still uses a higher
|
|
3581
|
+
* threshold (at least 1 inline template or at least 2 distinct permissions)
|
|
3582
|
+
* before it auto-nudges the developer.
|
|
3583
|
+
*
|
|
3584
|
+
* @example
|
|
3585
|
+
* ```typescript
|
|
3586
|
+
* const publishUrl = learnCard.getPublishUrl();
|
|
3587
|
+
* if (publishUrl) {
|
|
3588
|
+
* window.open(publishUrl, '_blank', 'noopener');
|
|
3589
|
+
* }
|
|
3590
|
+
* ```
|
|
3591
|
+
*/
|
|
3592
|
+
getPublishUrl() {
|
|
3593
|
+
var _a;
|
|
3594
|
+
return (_a = this.mockHost) == null ? void 0 : _a.getPublishUrl();
|
|
3595
|
+
}
|
|
3596
|
+
/** Clears this app's practice captures, credentials and counters. No-op outside mock mode. */
|
|
3597
|
+
resetPracticeMode() {
|
|
3598
|
+
var _a;
|
|
3599
|
+
(_a = this.mockHost) == null ? void 0 : _a.resetPracticeMode();
|
|
3600
|
+
}
|
|
3601
|
+
/**
|
|
3602
|
+
* Returns the currently active real-host origin, or `null` while standalone
|
|
3603
|
+
* mock mode is active.
|
|
3604
|
+
*/
|
|
3605
|
+
getActiveHostOrigin() {
|
|
3606
|
+
if (this.mockHost) return null;
|
|
3607
|
+
return this.activeHostOrigin || null;
|
|
3608
|
+
}
|
|
3609
|
+
/**
|
|
3610
|
+
* Returns the LearnCard origin used to build App Store publish URLs.
|
|
3611
|
+
*
|
|
3612
|
+
* Point this at a local or staging LearnCard with
|
|
3613
|
+
* `?lc_publish_override=<origin>`; see {@link resolvePublishOrigin} for
|
|
3614
|
+
* the full precedence order.
|
|
3615
|
+
*/
|
|
3616
|
+
getPublishOrigin() {
|
|
3617
|
+
if (this.mockHost) return this.mockHost.getPublishOrigin();
|
|
3618
|
+
return this.resolvePublishOrigin(this.hostOrigins);
|
|
3619
|
+
}
|
|
843
3620
|
/**
|
|
844
3621
|
* Read `window.location.ancestorOrigins[0]` without throwing if the
|
|
845
3622
|
* property is unavailable (Firefox) or the list is empty (top-level
|
|
@@ -917,6 +3694,12 @@ const _PartnerConnect = class _PartnerConnect {
|
|
|
917
3694
|
} catch {
|
|
918
3695
|
}
|
|
919
3696
|
}
|
|
3697
|
+
persistPublishOverride(origin) {
|
|
3698
|
+
try {
|
|
3699
|
+
sessionStorage.setItem(_PartnerConnect.PUBLISH_SESSION_STORAGE_KEY, origin);
|
|
3700
|
+
} catch {
|
|
3701
|
+
}
|
|
3702
|
+
}
|
|
920
3703
|
isOriginNativeApp(origin) {
|
|
921
3704
|
return origin.startsWith("capacitor://") || origin.startsWith("ionic://") || origin.startsWith("https://localhost") || origin.startsWith("http://localhost") || origin.startsWith("http://127.0.0.1");
|
|
922
3705
|
}
|
|
@@ -1121,12 +3904,58 @@ const _PartnerConnect = class _PartnerConnect {
|
|
|
1121
3904
|
challenge: `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
|
|
1122
3905
|
});
|
|
1123
3906
|
}
|
|
3907
|
+
/**
|
|
3908
|
+
* Validate an inline credential template entirely offline.
|
|
3909
|
+
*
|
|
3910
|
+
* This performs the same local checks that {@link sendCredential} runs for
|
|
3911
|
+
* inline templates before posting any message to LearnCard.
|
|
3912
|
+
*
|
|
3913
|
+
* @param template - Inline credential template to validate
|
|
3914
|
+
* @param templateData - Optional runtime variable values to validate against the compiled manifest
|
|
3915
|
+
* @returns Validation status plus path-based errors suitable for UI display
|
|
3916
|
+
*
|
|
3917
|
+
* @example
|
|
3918
|
+
* ```typescript
|
|
3919
|
+
* const result = learnCard.validateCredentialTemplate(
|
|
3920
|
+
* {
|
|
3921
|
+
* name: 'Completed {{courseName}}',
|
|
3922
|
+
* description: 'Awarded for finishing {{courseName}}.',
|
|
3923
|
+
* achievementType: 'Course',
|
|
3924
|
+
* criteria: { narrative: 'Finished all modules' }
|
|
3925
|
+
* },
|
|
3926
|
+
* { courseName: 'Intro to Baking' }
|
|
3927
|
+
* );
|
|
3928
|
+
*
|
|
3929
|
+
* if (!result.valid) {
|
|
3930
|
+
* console.error(result.errors.map(error => `${error.path}: ${error.message}`).join('\n'));
|
|
3931
|
+
* }
|
|
3932
|
+
* ```
|
|
3933
|
+
*/
|
|
3934
|
+
validateCredentialTemplate(template, templateData) {
|
|
3935
|
+
const templateErrors = validateInlineTemplate(template);
|
|
3936
|
+
if (templateErrors.length > 0) {
|
|
3937
|
+
return { valid: false, errors: templateErrors };
|
|
3938
|
+
}
|
|
3939
|
+
const compiled = compileInlineTemplate(template);
|
|
3940
|
+
const dataErrors = validateTemplateData(compiled.variableManifest, templateData);
|
|
3941
|
+
if (dataErrors.length > 0) {
|
|
3942
|
+
return { valid: false, errors: dataErrors };
|
|
3943
|
+
}
|
|
3944
|
+
return { valid: true, errors: [] };
|
|
3945
|
+
}
|
|
3946
|
+
/**
|
|
3947
|
+
* Compile and optionally render an inline credential template entirely offline.
|
|
3948
|
+
*/
|
|
3949
|
+
previewCompiledTemplate(template, templateData) {
|
|
3950
|
+
return previewCompiledTemplate(template, templateData);
|
|
3951
|
+
}
|
|
1124
3952
|
/**
|
|
1125
3953
|
* Send a credential to the user's LearnCard wallet
|
|
1126
3954
|
*
|
|
1127
3955
|
* Supports two modes:
|
|
1128
3956
|
* 1. **Raw credential**: Pass a full verifiable credential object
|
|
1129
3957
|
* 2. **Template-based**: Pass `{ templateAlias, templateData }` to issue from a pre-configured boost template
|
|
3958
|
+
* 3. **Inline template**: Pass `{ alias, template, templateData }` to issue from a zero-config inline template
|
|
1130
3959
|
*
|
|
1131
3960
|
* @param input - Either a verifiable credential or a template-based input
|
|
1132
3961
|
* @returns Promise resolving to credential response
|
|
@@ -1149,9 +3978,51 @@ const _PartnerConnect = class _PartnerConnect {
|
|
|
1149
3978
|
* });
|
|
1150
3979
|
* console.log('Credential URI:', response.credentialUri);
|
|
1151
3980
|
* ```
|
|
3981
|
+
*
|
|
3982
|
+
* @example Inline template (zero-config)
|
|
3983
|
+
* ```typescript
|
|
3984
|
+
* await learnCard.sendCredential({
|
|
3985
|
+
* alias: 'course-complete',
|
|
3986
|
+
* template: {
|
|
3987
|
+
* name: 'Completed {{courseName}}',
|
|
3988
|
+
* description: 'Awarded for finishing {{courseName}}.',
|
|
3989
|
+
* achievementType: 'Course',
|
|
3990
|
+
* criteria: { narrative: 'Finished all modules' }
|
|
3991
|
+
* },
|
|
3992
|
+
* templateData: { courseName: 'Intro to Baking' }
|
|
3993
|
+
* });
|
|
3994
|
+
* ```
|
|
1152
3995
|
*/
|
|
1153
3996
|
sendCredential(input) {
|
|
1154
|
-
if (input
|
|
3997
|
+
if (isInlineTemplateInput(input)) {
|
|
3998
|
+
const templateErrors = validateInlineTemplate(input.template);
|
|
3999
|
+
if (templateErrors.length > 0) {
|
|
4000
|
+
return Promise.reject(
|
|
4001
|
+
new PartnerConnectError(
|
|
4002
|
+
"TEMPLATE_INVALID",
|
|
4003
|
+
joinInlineTemplateErrors(templateErrors)
|
|
4004
|
+
)
|
|
4005
|
+
);
|
|
4006
|
+
}
|
|
4007
|
+
const compiled = compileInlineTemplate(input.template);
|
|
4008
|
+
const dataErrors = validateTemplateData(compiled.variableManifest, input.templateData);
|
|
4009
|
+
if (dataErrors.length > 0) {
|
|
4010
|
+
return Promise.reject(
|
|
4011
|
+
new PartnerConnectError(
|
|
4012
|
+
"TEMPLATE_DATA_INVALID",
|
|
4013
|
+
joinInlineTemplateErrors(dataErrors)
|
|
4014
|
+
)
|
|
4015
|
+
);
|
|
4016
|
+
}
|
|
4017
|
+
return this.sendAppEvent({
|
|
4018
|
+
type: "send-credential",
|
|
4019
|
+
alias: input.alias,
|
|
4020
|
+
template: input.template,
|
|
4021
|
+
templateData: input.templateData,
|
|
4022
|
+
preventDuplicateClaim: input.preventDuplicateClaim
|
|
4023
|
+
});
|
|
4024
|
+
}
|
|
4025
|
+
if (isTemplateAliasInput(input)) {
|
|
1155
4026
|
const templateInput = input;
|
|
1156
4027
|
return this.sendAppEvent({
|
|
1157
4028
|
type: "send-credential",
|
|
@@ -1301,36 +4172,26 @@ const _PartnerConnect = class _PartnerConnect {
|
|
|
1301
4172
|
credentialId
|
|
1302
4173
|
});
|
|
1303
4174
|
}
|
|
1304
|
-
|
|
1305
|
-
* Request user consent for permissions
|
|
1306
|
-
*
|
|
1307
|
-
* @param contractUri - URI of the consent contract (optional for App Store apps with configured contracts)
|
|
1308
|
-
* @param options - Additional options including redirect behavior
|
|
1309
|
-
* @returns Promise resolving to consent response
|
|
1310
|
-
*
|
|
1311
|
-
* @example
|
|
1312
|
-
* ```typescript
|
|
1313
|
-
* // With explicit contract URI (for external/non-app store integrations)
|
|
1314
|
-
* const response = await learnCard.requestConsent('lc:network:network.learncard.com/trpc:contract:abc123');
|
|
1315
|
-
* if (response.granted) {
|
|
1316
|
-
* console.log('User granted consent');
|
|
1317
|
-
* }
|
|
1318
|
-
*
|
|
1319
|
-
* // Without contract URI (uses app's configured contract from integration)
|
|
1320
|
-
* // This works for App Store apps that have configured a contract in their integration
|
|
1321
|
-
* const response = await learnCard.requestConsent();
|
|
1322
|
-
* if (response.granted) {
|
|
1323
|
-
* console.log('User granted consent using listing contract');
|
|
1324
|
-
* }
|
|
1325
|
-
*
|
|
1326
|
-
* // With redirect - redirects to contract's redirectUrl with VP in URL params
|
|
1327
|
-
* const response = await learnCard.requestConsent(undefined, { redirect: true });
|
|
1328
|
-
* ```
|
|
1329
|
-
*/
|
|
1330
|
-
requestConsent(contractUri, options = {}) {
|
|
4175
|
+
requestConsent(requestOrContractUri, options = {}) {
|
|
1331
4176
|
const { redirect = false } = options;
|
|
4177
|
+
if (isConsentRequestInput(requestOrContractUri)) {
|
|
4178
|
+
try {
|
|
4179
|
+
normalizeConsentRequest(requestOrContractUri);
|
|
4180
|
+
} catch (error) {
|
|
4181
|
+
return Promise.reject(
|
|
4182
|
+
new PartnerConnectError(
|
|
4183
|
+
"CONSENT_SCOPES_INVALID",
|
|
4184
|
+
error instanceof Error ? error.message : "Invalid consent scopes"
|
|
4185
|
+
)
|
|
4186
|
+
);
|
|
4187
|
+
}
|
|
4188
|
+
return this.sendMessage("REQUEST_CONSENT", {
|
|
4189
|
+
scopes: requestOrContractUri,
|
|
4190
|
+
redirect
|
|
4191
|
+
});
|
|
4192
|
+
}
|
|
1332
4193
|
return this.sendMessage("REQUEST_CONSENT", {
|
|
1333
|
-
contractUri,
|
|
4194
|
+
contractUri: requestOrContractUri,
|
|
1334
4195
|
redirect
|
|
1335
4196
|
});
|
|
1336
4197
|
}
|
|
@@ -1612,6 +4473,8 @@ __publicField(_PartnerConnect, "DEFAULT_TRUSTED_TENANTS", [
|
|
|
1612
4473
|
* continue to use the same active origin.
|
|
1613
4474
|
*/
|
|
1614
4475
|
__publicField(_PartnerConnect, "SESSION_STORAGE_KEY", "lc_host_override");
|
|
4476
|
+
/** Persisted so the parameter only has to be typed once per tab. */
|
|
4477
|
+
__publicField(_PartnerConnect, "PUBLISH_SESSION_STORAGE_KEY", "lc_publish_override");
|
|
1615
4478
|
/**
|
|
1616
4479
|
* Internal placeholder substituted in for `*` so that `new URL(...)` can
|
|
1617
4480
|
* parse a wildcard pattern. Chosen to be a syntactically-valid DNS label
|
|
@@ -1630,6 +4493,8 @@ function createPartnerConnect(options) {
|
|
|
1630
4493
|
exports.PartnerConnect = PartnerConnect;
|
|
1631
4494
|
exports.PartnerConnectError = PartnerConnectError;
|
|
1632
4495
|
exports.createPartnerConnect = createPartnerConnect;
|
|
4496
|
+
exports.decodeManifestFromUrl = decodeManifestFromUrl;
|
|
1633
4497
|
exports["default"] = createPartnerConnect;
|
|
1634
4498
|
exports.isEmbedded = isEmbedded;
|
|
4499
|
+
exports.previewCompiledTemplate = previewCompiledTemplate;
|
|
1635
4500
|
//# sourceMappingURL=partner-connect.js.map
|