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