@learncard/partner-connect 0.5.1 → 0.6.0

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