@learncard/partner-connect 0.5.2 → 0.6.1

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