@learncard/cli 3.4.16 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/README.md +10 -0
- package/dist/index.js +1933 -56
- package/package.json +24 -24
- package/rollup.config.js +25 -3
- package/scripts/embed-snippets.ts +25 -0
- package/src/consent-contract.ts +116 -0
- package/src/embed.ts +102 -0
- package/src/index.tsx +473 -147
- package/src/init.ts +26 -0
- package/src/open.test.ts +40 -0
- package/src/open.ts +186 -0
- package/src/out.test.ts +40 -0
- package/src/out.ts +16 -0
- package/src/phase-two.test.ts +154 -0
- package/src/project.test.ts +207 -0
- package/src/project.ts +394 -0
- package/src/revoke.test.ts +29 -0
- package/src/revoke.ts +65 -0
- package/src/send-template.test.ts +20 -0
- package/src/send.test.ts +45 -0
- package/src/send.ts +245 -0
- package/src/setup-signing.test.ts +62 -0
- package/src/setup-signing.ts +185 -0
- package/src/snippet-files.ts +16 -0
- package/src/status.test.ts +33 -0
- package/src/status.ts +96 -0
- package/src/token.test.ts +28 -0
- package/src/token.ts +138 -0
- package/src/verify.test.ts +35 -0
- package/src/verify.ts +66 -0
- package/src/webhook.ts +247 -0
- package/tsconfig.json +1 -0
package/dist/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
var fs = require('fs/promises');
|
|
4
|
+
var fs$1 = require('fs/promises');
|
|
5
5
|
var dns = require('node:dns');
|
|
6
6
|
var promises = require('node:readline/promises');
|
|
7
7
|
var node_stream = require('node:stream');
|
|
8
8
|
var node_util = require('node:util');
|
|
9
9
|
var core = require('@learncard/core');
|
|
10
|
-
var init = require('@learncard/init');
|
|
10
|
+
var init$1 = require('@learncard/init');
|
|
11
11
|
var lcaApiPlugin = require('@learncard/lca-api-plugin');
|
|
12
12
|
var openBadgeV2Plugin = require('@learncard/open-badge-v2-plugin');
|
|
13
13
|
var types = require('@learncard/types');
|
|
@@ -18,8 +18,13 @@ var commander = require('commander');
|
|
|
18
18
|
var clipboard = require('clipboardy');
|
|
19
19
|
var lerRsPlugin = require('@learncard/ler-rs-plugin');
|
|
20
20
|
var renderMethodPlugin = require('@learncard/render-method-plugin');
|
|
21
|
-
var crypto = require('crypto');
|
|
22
21
|
var holderContinuity = require('@learncard/holder-continuity');
|
|
22
|
+
var fs = require('node:fs/promises');
|
|
23
|
+
var path = require('node:path');
|
|
24
|
+
var node_child_process = require('node:child_process');
|
|
25
|
+
var path$1 = require('path');
|
|
26
|
+
var node_crypto = require('node:crypto');
|
|
27
|
+
var crypto = require('crypto');
|
|
23
28
|
|
|
24
29
|
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
|
25
30
|
|
|
@@ -41,35 +46,749 @@ function _interopNamespace(e) {
|
|
|
41
46
|
return Object.freeze(n);
|
|
42
47
|
}
|
|
43
48
|
|
|
44
|
-
var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs);
|
|
49
|
+
var fs__default$1 = /*#__PURE__*/_interopDefaultLegacy(fs$1);
|
|
45
50
|
var dns__default = /*#__PURE__*/_interopDefaultLegacy(dns);
|
|
46
51
|
var types__namespace = /*#__PURE__*/_interopNamespace(types);
|
|
47
52
|
var gradient__default = /*#__PURE__*/_interopDefaultLegacy(gradient);
|
|
48
53
|
var figlet__default = /*#__PURE__*/_interopDefaultLegacy(figlet);
|
|
49
54
|
var clipboard__default = /*#__PURE__*/_interopDefaultLegacy(clipboard);
|
|
55
|
+
var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs);
|
|
56
|
+
var path__default = /*#__PURE__*/_interopDefaultLegacy(path);
|
|
57
|
+
var path__default$1 = /*#__PURE__*/_interopDefaultLegacy(path$1);
|
|
50
58
|
var crypto__default = /*#__PURE__*/_interopDefaultLegacy(crypto);
|
|
51
59
|
|
|
52
60
|
const generateRandomSeed = () => crypto__default["default"].randomBytes(32).toString("hex");
|
|
53
61
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
var
|
|
62
|
+
const out = {
|
|
63
|
+
json: false,
|
|
64
|
+
log: (...a) => out.json ? console.error(...a) : console.log(...a),
|
|
65
|
+
result: {},
|
|
66
|
+
set: (patch) => Object.assign(out.result, patch)
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
var __defProp$a = Object.defineProperty;
|
|
70
|
+
var __defProps$8 = Object.defineProperties;
|
|
71
|
+
var __getOwnPropDescs$8 = Object.getOwnPropertyDescriptors;
|
|
72
|
+
var __getOwnPropSymbols$a = Object.getOwnPropertySymbols;
|
|
73
|
+
var __hasOwnProp$a = Object.prototype.hasOwnProperty;
|
|
74
|
+
var __propIsEnum$a = Object.prototype.propertyIsEnumerable;
|
|
75
|
+
var __defNormalProp$a = (obj, key, value) => key in obj ? __defProp$a(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
76
|
+
var __spreadValues$a = (a, b) => {
|
|
62
77
|
for (var prop in b || (b = {}))
|
|
63
|
-
if (__hasOwnProp.call(b, prop))
|
|
64
|
-
__defNormalProp(a, prop, b[prop]);
|
|
65
|
-
if (__getOwnPropSymbols)
|
|
66
|
-
for (var prop of __getOwnPropSymbols(b)) {
|
|
67
|
-
if (__propIsEnum.call(b, prop))
|
|
68
|
-
__defNormalProp(a, prop, b[prop]);
|
|
78
|
+
if (__hasOwnProp$a.call(b, prop))
|
|
79
|
+
__defNormalProp$a(a, prop, b[prop]);
|
|
80
|
+
if (__getOwnPropSymbols$a)
|
|
81
|
+
for (var prop of __getOwnPropSymbols$a(b)) {
|
|
82
|
+
if (__propIsEnum$a.call(b, prop))
|
|
83
|
+
__defNormalProp$a(a, prop, b[prop]);
|
|
69
84
|
}
|
|
70
85
|
return a;
|
|
71
86
|
};
|
|
72
|
-
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
87
|
+
var __spreadProps$8 = (a, b) => __defProps$8(a, __getOwnPropDescs$8(b));
|
|
88
|
+
const KEYS = {
|
|
89
|
+
SECURE_SEED: "SECURE_SEED",
|
|
90
|
+
// Private issuer seed; never regenerate or print.
|
|
91
|
+
PROFILE_ID: "PROFILE_ID",
|
|
92
|
+
// Public issuer handle shared by all commands.
|
|
93
|
+
DISPLAY_NAME: "DISPLAY_NAME",
|
|
94
|
+
// Issuer display name; set at creation, then synced from the network.
|
|
95
|
+
NETWORK_URL: "NETWORK_URL",
|
|
96
|
+
// Non-default network tRPC endpoint.
|
|
97
|
+
SIGNING_AUTHORITY_NAME: "SIGNING_AUTHORITY_NAME",
|
|
98
|
+
// Registered primary signer name.
|
|
99
|
+
SIGNING_AUTHORITY_ENDPOINT: "SIGNING_AUTHORITY_ENDPOINT",
|
|
100
|
+
// Hosted signing endpoint.
|
|
101
|
+
API_TOKEN: "API_TOKEN",
|
|
102
|
+
// Secret bearer token for REST requests.
|
|
103
|
+
API_TOKEN_SCOPE: "API_TOKEN_SCOPE",
|
|
104
|
+
// Space-separated permissions granted to the token.
|
|
105
|
+
TEMPLATE_URI: "TEMPLATE_URI",
|
|
106
|
+
// Reusable Boost template on this network.
|
|
107
|
+
CONTRACT_URI: "CONTRACT_URI",
|
|
108
|
+
// Consent contract for later commands.
|
|
109
|
+
PUBLISHABLE_KEY: "PUBLISHABLE_KEY",
|
|
110
|
+
// Public integration client key.
|
|
111
|
+
INTEGRATION_ID: "INTEGRATION_ID"
|
|
112
|
+
// Developer integration identifier.
|
|
113
|
+
};
|
|
114
|
+
const parseEnv = (text) => {
|
|
115
|
+
const env = {};
|
|
116
|
+
for (const line of text.split("\n")) {
|
|
117
|
+
const match = line.match(/^\s*(?:export\s+)?([\w]+)\s*=\s*(.*)$/);
|
|
118
|
+
if (!match) continue;
|
|
119
|
+
const key = match[1];
|
|
120
|
+
let value = match[2].trim();
|
|
121
|
+
if (value.startsWith('"') || value.startsWith("'")) {
|
|
122
|
+
const end = value.indexOf(value[0], 1);
|
|
123
|
+
if (end < 0) throw new Error(`Unclosed quote in .env for ${key}.`);
|
|
124
|
+
value = value.slice(1, end);
|
|
125
|
+
} else value = value.split("#")[0].trim();
|
|
126
|
+
if (key === "SECURE_SEED" && env[key] !== void 0 && env[key] !== value) {
|
|
127
|
+
throw new Error("Conflicting SECURE_SEED entries in .env. Keep the original identity.");
|
|
128
|
+
}
|
|
129
|
+
Object.defineProperty(env, key, {
|
|
130
|
+
value,
|
|
131
|
+
enumerable: true,
|
|
132
|
+
configurable: true,
|
|
133
|
+
writable: true
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return env;
|
|
137
|
+
};
|
|
138
|
+
const upsertEnv = (text, values) => {
|
|
139
|
+
const lines = text ? text.replace(/\n$/, "").split("\n") : [];
|
|
140
|
+
const seen = /* @__PURE__ */ new Set();
|
|
141
|
+
const out2 = lines.map((line) => {
|
|
142
|
+
var _a;
|
|
143
|
+
const key = (_a = line.match(/^\s*(?:export\s+)?([\w]+)\s*=/)) == null ? void 0 : _a[1];
|
|
144
|
+
if (key && Object.prototype.hasOwnProperty.call(values, key)) {
|
|
145
|
+
seen.add(key);
|
|
146
|
+
const value = values[key];
|
|
147
|
+
if (/[\r\n]/.test(value)) throw new Error(`Invalid multiline value for ${key}`);
|
|
148
|
+
return `${key}=${/^[a-zA-Z0-9_./:@*=%+-]*$/.test(value) ? value : JSON.stringify(value)}`;
|
|
149
|
+
}
|
|
150
|
+
return line;
|
|
151
|
+
});
|
|
152
|
+
for (const [key, value] of Object.entries(values)) {
|
|
153
|
+
if (!seen.has(key)) out2.push(upsertEnv(`${key}=
|
|
154
|
+
`, { [key]: value }).trimEnd());
|
|
155
|
+
}
|
|
156
|
+
return `${out2.join("\n")}
|
|
157
|
+
`;
|
|
158
|
+
};
|
|
159
|
+
const toProfileId = (displayName) => {
|
|
160
|
+
const base = displayName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 30);
|
|
161
|
+
return `${base || "issuer"}-${Math.random().toString(36).slice(2, 6)}`;
|
|
162
|
+
};
|
|
163
|
+
const readOptional = async (file) => {
|
|
164
|
+
try {
|
|
165
|
+
return await fs__default["default"].readFile(file, "utf8");
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if (error.code === "ENOENT") return "";
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
const loadProject = async (cwd) => {
|
|
172
|
+
const envPath = path__default["default"].join(cwd, ".env");
|
|
173
|
+
await assertRegularEnv(envPath);
|
|
174
|
+
const existing = await readOptional(envPath);
|
|
175
|
+
return { env: parseEnv(existing), envPath, existing };
|
|
176
|
+
};
|
|
177
|
+
const assertRegularEnv = async (envPath) => {
|
|
178
|
+
const info = await fs__default["default"].lstat(envPath).catch((error) => {
|
|
179
|
+
if (error.code === "ENOENT") return void 0;
|
|
180
|
+
throw error;
|
|
181
|
+
});
|
|
182
|
+
if (info && !info.isFile()) throw new Error(".env must be a regular file, not a symlink.");
|
|
183
|
+
};
|
|
184
|
+
const saveProject = async (project, updates) => {
|
|
185
|
+
const lockPath = `${project.envPath}.lock`;
|
|
186
|
+
const lock = await fs__default["default"].open(lockPath, "wx", 384).catch((error) => {
|
|
187
|
+
if (error.code === "EEXIST")
|
|
188
|
+
throw new Error(
|
|
189
|
+
"Another command is updating .env. Retry when it finishes; remove a stale .env.lock only if no command is running."
|
|
190
|
+
);
|
|
191
|
+
throw error;
|
|
192
|
+
});
|
|
193
|
+
const temporary = `${project.envPath}.${node_crypto.randomUUID()}.tmp`;
|
|
194
|
+
try {
|
|
195
|
+
await assertRegularEnv(project.envPath);
|
|
196
|
+
const existing = await readOptional(project.envPath);
|
|
197
|
+
const env = parseEnv(existing);
|
|
198
|
+
if (env.SECURE_SEED && updates.SECURE_SEED !== void 0 && env.SECURE_SEED !== updates.SECURE_SEED) {
|
|
199
|
+
throw new Error("Cannot replace an existing SECURE_SEED.");
|
|
200
|
+
}
|
|
201
|
+
const changed = Object.fromEntries(
|
|
202
|
+
Object.entries(updates).filter(([key, value]) => env[key] !== value)
|
|
203
|
+
);
|
|
204
|
+
project.existing = existing;
|
|
205
|
+
project.env = env;
|
|
206
|
+
if (!Object.keys(changed).length) return;
|
|
207
|
+
const next = upsertEnv(existing, changed);
|
|
208
|
+
await fs__default["default"].writeFile(temporary, next, { mode: 384, flag: "wx" });
|
|
209
|
+
await fs__default["default"].rename(temporary, project.envPath);
|
|
210
|
+
project.existing = next;
|
|
211
|
+
Object.assign(project.env, changed);
|
|
212
|
+
const keys = Object.keys(changed);
|
|
213
|
+
const list = keys.length > 2 ? `${keys.slice(0, -1).join(", ")}, and ${keys.at(-1)}` : keys.join(" and ");
|
|
214
|
+
out.log(`Wrote ${list} to .env`);
|
|
215
|
+
} finally {
|
|
216
|
+
await fs__default["default"].rm(temporary, { force: true });
|
|
217
|
+
await lock.close();
|
|
218
|
+
await fs__default["default"].unlink(lockPath);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
const createPrompts = (yes) => {
|
|
222
|
+
const interactive = !yes && !!process.stdin.isTTY && process.env.LC_YES !== "1";
|
|
223
|
+
const rl = interactive ? promises.createInterface({ input: process.stdin, output: process.stdout }) : null;
|
|
224
|
+
return {
|
|
225
|
+
ask: async (question, fallback) => {
|
|
226
|
+
if (rl) return (await rl.question(`${question} [${fallback}] `)).trim() || fallback;
|
|
227
|
+
if (!fallback)
|
|
228
|
+
throw new Error(
|
|
229
|
+
`${question} is required when running non-interactively. Pass it as an argument or flag.`
|
|
230
|
+
);
|
|
231
|
+
return fallback;
|
|
232
|
+
},
|
|
233
|
+
close: () => {
|
|
234
|
+
rl == null ? void 0 : rl.close();
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
};
|
|
238
|
+
const ensureIdentity = async (project, options) => {
|
|
239
|
+
var _a, _b;
|
|
240
|
+
const existingProfileId = project.env.PROFILE_ID || options.profileId;
|
|
241
|
+
let displayName = (_b = (_a = options.name) != null ? _a : project.env.DISPLAY_NAME) != null ? _b : "";
|
|
242
|
+
if (!existingProfileId && !displayName) {
|
|
243
|
+
const prompts = createPrompts(options.yes);
|
|
244
|
+
try {
|
|
245
|
+
displayName = await prompts.ask(
|
|
246
|
+
"Display name for your issuer profile",
|
|
247
|
+
"My Organization"
|
|
248
|
+
);
|
|
249
|
+
} finally {
|
|
250
|
+
prompts.close();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (!displayName) displayName = "My Organization";
|
|
254
|
+
if (!project.env.SECURE_SEED) {
|
|
255
|
+
const { network } = resolveServices(project.env, options.network);
|
|
256
|
+
const where = network === PRODUCTION_NETWORK ? "production" : network === STAGING_NETWORK ? "staging" : network;
|
|
257
|
+
out.log(`No .env here \u2014 creating a new ${where} identity in ${process.cwd()}`);
|
|
258
|
+
}
|
|
259
|
+
const seed = project.env.SECURE_SEED || generateRandomSeed();
|
|
260
|
+
const profileId = project.env.PROFILE_ID || options.profileId || toProfileId(displayName);
|
|
261
|
+
await saveProject(project, __spreadValues$a({
|
|
262
|
+
SECURE_SEED: seed,
|
|
263
|
+
PROFILE_ID: profileId
|
|
264
|
+
}, existingProfileId ? {} : { DISPLAY_NAME: displayName }));
|
|
265
|
+
const gitignorePath = path__default["default"].join(path__default["default"].dirname(project.envPath), ".gitignore");
|
|
266
|
+
const gitignore = await readOptional(gitignorePath);
|
|
267
|
+
if (await fs__default["default"].stat(gitignorePath).catch(() => null)) {
|
|
268
|
+
if (!gitignore.split("\n").some((line) => line.trim() === ".env")) {
|
|
269
|
+
await fs__default["default"].writeFile(gitignorePath, `${gitignore.replace(/\n?$/, "\n")}.env
|
|
270
|
+
`);
|
|
271
|
+
out.log("Added .env to .gitignore");
|
|
272
|
+
}
|
|
273
|
+
} else {
|
|
274
|
+
await fs__default["default"].writeFile(gitignorePath, ".env\n");
|
|
275
|
+
out.log("Created .gitignore with .env");
|
|
276
|
+
}
|
|
277
|
+
return { seed, profileId, displayName };
|
|
278
|
+
};
|
|
279
|
+
const PRODUCTION_NETWORK = "https://network.learncard.com/trpc";
|
|
280
|
+
const STAGING_NETWORK = "https://staging.network.learncard.com/trpc";
|
|
281
|
+
const assertProjectNetwork = (project, network) => {
|
|
282
|
+
const previous = project.env.NETWORK_URL === "staging" ? STAGING_NETWORK : project.env.NETWORK_URL || PRODUCTION_NETWORK;
|
|
283
|
+
const hasResources = [
|
|
284
|
+
"SIGNING_AUTHORITY_NAME",
|
|
285
|
+
"API_TOKEN",
|
|
286
|
+
"TEMPLATE_URI",
|
|
287
|
+
"CONTRACT_URI",
|
|
288
|
+
"PUBLISHABLE_KEY",
|
|
289
|
+
"INTEGRATION_ID"
|
|
290
|
+
].some((key) => project.env[key]);
|
|
291
|
+
if (previous !== network && hasResources) {
|
|
292
|
+
throw new Error(
|
|
293
|
+
"This project has resources on another network. Use a separate folder for staging or another network."
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
const resolveServices = (env, network, runtime = process.env) => {
|
|
298
|
+
const requested = network || runtime.NETWORK_URL || env.NETWORK_URL || PRODUCTION_NETWORK;
|
|
299
|
+
const url = requested === "staging" ? STAGING_NETWORK : requested;
|
|
300
|
+
const parsed = new URL(url);
|
|
301
|
+
if (!["http:", "https:"].includes(parsed.protocol))
|
|
302
|
+
throw new Error("Network must be an HTTP(S) tRPC URL or staging.");
|
|
303
|
+
const staging = url === STAGING_NETWORK;
|
|
304
|
+
return {
|
|
305
|
+
network: url,
|
|
306
|
+
cloud: runtime.CLOUD_URL || env.CLOUD_URL || (staging ? "https://staging.cloud.learncard.com/trpc" : void 0),
|
|
307
|
+
lcaAPI: runtime.LCA_API_URL || env.LCA_API_URL || (staging ? "https://staging.api.learncard.app/trpc" : void 0)
|
|
308
|
+
};
|
|
309
|
+
};
|
|
310
|
+
const PRODUCTION_APP = "https://learncard.app";
|
|
311
|
+
const STAGING_APP = "https://staging.learncard.ai";
|
|
312
|
+
const appUrlFor = (network, override) => {
|
|
313
|
+
if (override) return override.replace(/\/$/, "");
|
|
314
|
+
if (network === STAGING_NETWORK) return STAGING_APP;
|
|
315
|
+
if (network === PRODUCTION_NETWORK) return PRODUCTION_APP;
|
|
316
|
+
return PRODUCTION_APP;
|
|
317
|
+
};
|
|
318
|
+
const localizeSnippet = (source, services) => {
|
|
319
|
+
if (services.network === PRODUCTION_NETWORK) return source;
|
|
320
|
+
const cloud = services.cloud ? `, cloud: { url: '${services.cloud}' }` : "";
|
|
321
|
+
return source.replace(
|
|
322
|
+
/initLearnCard\(\{ seed: process\.env\.SECURE_SEED, network: true \}\)/g,
|
|
323
|
+
`initLearnCard({ seed: process.env.SECURE_SEED, network: '${services.network}'${cloud} })`
|
|
324
|
+
).replace(
|
|
325
|
+
"https://network.learncard.com/api/send",
|
|
326
|
+
`${services.network.replace(/\/trpc$/, "")}/api/send`
|
|
327
|
+
);
|
|
328
|
+
};
|
|
329
|
+
const ENV_TOKEN_LOADER = `#!/bin/sh
|
|
330
|
+
set -eu
|
|
331
|
+
# Read API_TOKEN from .env without executing it (strips optional quotes).
|
|
332
|
+
if [ -f .env ]; then
|
|
333
|
+
API_TOKEN=$(sed -n 's/^API_TOKEN=//p' .env | sed 's/^["'"'"']//; s/["'"'"']$//')
|
|
334
|
+
fi
|
|
335
|
+
TOKEN=\${TOKEN:-\${API_TOKEN:-}}
|
|
336
|
+
: "\${TOKEN:?Run npx @learncard/cli token first}"
|
|
337
|
+
|
|
338
|
+
`;
|
|
339
|
+
const withEnvTokenLoader = (sendSh) => ENV_TOKEN_LOADER + sendSh;
|
|
340
|
+
async function connect(project, options) {
|
|
341
|
+
const seed = project.env.SECURE_SEED;
|
|
342
|
+
if (!seed) throw new Error("Create an identity before connecting.");
|
|
343
|
+
const services = resolveServices(project.env, options.network);
|
|
344
|
+
assertProjectNetwork(project, services.network);
|
|
345
|
+
if (services.network !== PRODUCTION_NETWORK || project.env.NETWORK_URL) {
|
|
346
|
+
await saveProject(project, {
|
|
347
|
+
NETWORK_URL: services.network === PRODUCTION_NETWORK ? "" : services.network
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
out.log("Connecting to the LearnCard Network...");
|
|
351
|
+
const config = __spreadValues$a(__spreadValues$a({
|
|
352
|
+
seed,
|
|
353
|
+
network: services.network
|
|
354
|
+
}, services.cloud && { cloud: { url: services.cloud } }), options.didkit && { didkit: options.didkit });
|
|
355
|
+
return options.lca ? lcaApiPlugin.initLCALearnCard(__spreadValues$a(__spreadValues$a({}, config), services.lcaAPI && { lcaAPI: services.lcaAPI.replace(/\/api\/?$/, "/trpc") })) : init$1.initLearnCard(__spreadProps$8(__spreadValues$a({}, config), {
|
|
356
|
+
network: services.network === PRODUCTION_NETWORK ? true : services.network
|
|
357
|
+
}));
|
|
358
|
+
}
|
|
359
|
+
const ensureProfile = async (learnCard, identity, project) => {
|
|
360
|
+
const existing = await learnCard.invoke.getProfile();
|
|
361
|
+
if (existing) {
|
|
362
|
+
out.log(`Signed in as "${existing.displayName}" (${existing.profileId})`);
|
|
363
|
+
if (project && project.env.DISPLAY_NAME !== existing.displayName)
|
|
364
|
+
await saveProject(project, { DISPLAY_NAME: existing.displayName });
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
await learnCard.invoke.createProfile({
|
|
368
|
+
profileId: identity.profileId,
|
|
369
|
+
displayName: identity.displayName,
|
|
370
|
+
bio: "",
|
|
371
|
+
shortBio: ""
|
|
372
|
+
});
|
|
373
|
+
out.log(`Created profile "${identity.displayName}" (${identity.profileId})`);
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
var __defProp$9 = Object.defineProperty;
|
|
377
|
+
var __defProps$7 = Object.defineProperties;
|
|
378
|
+
var __getOwnPropDescs$7 = Object.getOwnPropertyDescriptors;
|
|
379
|
+
var __getOwnPropSymbols$9 = Object.getOwnPropertySymbols;
|
|
380
|
+
var __hasOwnProp$9 = Object.prototype.hasOwnProperty;
|
|
381
|
+
var __propIsEnum$9 = Object.prototype.propertyIsEnumerable;
|
|
382
|
+
var __defNormalProp$9 = (obj, key, value) => key in obj ? __defProp$9(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
383
|
+
var __spreadValues$9 = (a, b) => {
|
|
384
|
+
for (var prop in b || (b = {}))
|
|
385
|
+
if (__hasOwnProp$9.call(b, prop))
|
|
386
|
+
__defNormalProp$9(a, prop, b[prop]);
|
|
387
|
+
if (__getOwnPropSymbols$9)
|
|
388
|
+
for (var prop of __getOwnPropSymbols$9(b)) {
|
|
389
|
+
if (__propIsEnum$9.call(b, prop))
|
|
390
|
+
__defNormalProp$9(a, prop, b[prop]);
|
|
391
|
+
}
|
|
392
|
+
return a;
|
|
393
|
+
};
|
|
394
|
+
var __spreadProps$7 = (a, b) => __defProps$7(a, __getOwnPropDescs$7(b));
|
|
395
|
+
const authorityName = (project, name) => {
|
|
396
|
+
var _a;
|
|
397
|
+
return (_a = name != null ? name : project.env.SIGNING_AUTHORITY_NAME) != null ? _a : "default-issuer";
|
|
398
|
+
};
|
|
399
|
+
const setupSigning = async (project, learnCard, requestedName, options = {}) => {
|
|
400
|
+
const name = authorityName(project, requestedName);
|
|
401
|
+
const registered = (await learnCard.invoke.getRegisteredSigningAuthorities()).find(
|
|
402
|
+
(authority2) => authority2.relationship.name === name && (requestedName || !project.env.SIGNING_AUTHORITY_ENDPOINT || authority2.signingAuthority.endpoint === project.env.SIGNING_AUTHORITY_ENDPOINT)
|
|
403
|
+
);
|
|
404
|
+
if (registered) {
|
|
405
|
+
const endpoint = registered.signingAuthority.endpoint;
|
|
406
|
+
if (!registered.relationship.isPrimary) {
|
|
407
|
+
if (!await learnCard.invoke.setPrimaryRegisteredSigningAuthority(endpoint, name)) {
|
|
408
|
+
throw new Error(
|
|
409
|
+
"Could not select the primary signing authority. Please try again."
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (options.persist !== false) {
|
|
414
|
+
await saveProject(project, {
|
|
415
|
+
SIGNING_AUTHORITY_NAME: name,
|
|
416
|
+
SIGNING_AUTHORITY_ENDPOINT: endpoint
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
out.log(`Signing authority "${name}" is already your primary.`);
|
|
420
|
+
return { name, endpoint, did: registered.relationship.did, alreadyConfigured: true };
|
|
421
|
+
}
|
|
422
|
+
const hosted = await learnCard.invoke.getSigningAuthorities();
|
|
423
|
+
if (!hosted) throw new Error("Could not look up hosted signing authorities. Please try again.");
|
|
424
|
+
const existing = hosted.find((authority2) => authority2.name === name);
|
|
425
|
+
const authority = existing || await learnCard.invoke.createSigningAuthority(name);
|
|
426
|
+
if (!authority || !authority.endpoint || !authority.did)
|
|
427
|
+
throw new Error("Could not create signing authority. Please try again.");
|
|
428
|
+
const registeredSuccessfully = await learnCard.invoke.registerSigningAuthority(
|
|
429
|
+
authority.endpoint,
|
|
430
|
+
authority.name,
|
|
431
|
+
authority.did
|
|
432
|
+
);
|
|
433
|
+
if (!registeredSuccessfully)
|
|
434
|
+
throw new Error("Could not register signing authority. Please try again.");
|
|
435
|
+
if (!await learnCard.invoke.setPrimaryRegisteredSigningAuthority(
|
|
436
|
+
authority.endpoint,
|
|
437
|
+
authority.name
|
|
438
|
+
)) {
|
|
439
|
+
throw new Error("Could not select the primary signing authority. Please try again.");
|
|
440
|
+
}
|
|
441
|
+
if (options.persist !== false) {
|
|
442
|
+
await saveProject(project, {
|
|
443
|
+
SIGNING_AUTHORITY_NAME: authority.name,
|
|
444
|
+
SIGNING_AUTHORITY_ENDPOINT: authority.endpoint
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
out.log(`LearnCard will now sign credentials for ${project.env.PROFILE_ID}.`);
|
|
448
|
+
out.log("What this did:");
|
|
449
|
+
out.log(
|
|
450
|
+
existing ? ` const authority = (await learnCard.invoke.getSigningAuthorities()).find(authority => authority.name === ${JSON.stringify(name)});` : ` const authority = await learnCard.invoke.createSigningAuthority(${JSON.stringify(name)});`
|
|
451
|
+
);
|
|
452
|
+
out.log(
|
|
453
|
+
" await learnCard.invoke.registerSigningAuthority(authority.endpoint, authority.name, authority.did);"
|
|
454
|
+
);
|
|
455
|
+
out.log(
|
|
456
|
+
" await learnCard.invoke.setPrimaryRegisteredSigningAuthority(authority.endpoint, authority.name);"
|
|
457
|
+
);
|
|
458
|
+
return {
|
|
459
|
+
name: authority.name,
|
|
460
|
+
endpoint: authority.endpoint,
|
|
461
|
+
did: authority.did,
|
|
462
|
+
alreadyConfigured: false
|
|
463
|
+
};
|
|
464
|
+
};
|
|
465
|
+
const registerOwnAuthority = async (project, learnCard, name, endpoint, did) => {
|
|
466
|
+
if (!/^https:\/\//.test(endpoint)) throw new Error("--endpoint must be an https:// URL.");
|
|
467
|
+
if (!/^did:/.test(did)) throw new Error("--did must be a DID (did:web:..., did:key:...).");
|
|
468
|
+
if (!await learnCard.invoke.registerSigningAuthority(endpoint, name, did))
|
|
469
|
+
throw new Error("Could not register the signing authority.");
|
|
470
|
+
if (!await learnCard.invoke.setPrimaryRegisteredSigningAuthority(endpoint, name))
|
|
471
|
+
throw new Error("Could not set the signing authority as primary.");
|
|
472
|
+
await saveProject(project, {
|
|
473
|
+
SIGNING_AUTHORITY_NAME: name,
|
|
474
|
+
SIGNING_AUTHORITY_ENDPOINT: endpoint
|
|
475
|
+
});
|
|
476
|
+
out.log(`Your service at ${endpoint} will now sign credentials for this profile.`);
|
|
477
|
+
out.log("What this did:");
|
|
478
|
+
out.log(
|
|
479
|
+
` await learnCard.invoke.registerSigningAuthority(${JSON.stringify(endpoint)}, ${JSON.stringify(name)}, ${JSON.stringify(did)});`
|
|
480
|
+
);
|
|
481
|
+
out.log(
|
|
482
|
+
` await learnCard.invoke.setPrimaryRegisteredSigningAuthority(${JSON.stringify(endpoint)}, ${JSON.stringify(name)});`
|
|
483
|
+
);
|
|
484
|
+
return { name, endpoint, did, alreadyConfigured: false };
|
|
485
|
+
};
|
|
486
|
+
const runSetupSigning = async (options) => {
|
|
487
|
+
var _a;
|
|
488
|
+
const project = await loadProject(process.cwd());
|
|
489
|
+
const identity = await ensureIdentity(project, __spreadProps$7(__spreadValues$9({}, options), { name: void 0 }));
|
|
490
|
+
if (options.endpoint && !options.did || !options.endpoint && options.did)
|
|
491
|
+
throw new Error("Pass both --endpoint and --did to register your own signing service.");
|
|
492
|
+
let authority;
|
|
493
|
+
if (options.endpoint) {
|
|
494
|
+
const learnCard = await connect(project, options);
|
|
495
|
+
await ensureProfile(learnCard, identity, project);
|
|
496
|
+
authority = await registerOwnAuthority(
|
|
497
|
+
project,
|
|
498
|
+
learnCard,
|
|
499
|
+
(_a = options.name) != null ? _a : "my-issuer",
|
|
500
|
+
options.endpoint,
|
|
501
|
+
options.did
|
|
502
|
+
);
|
|
503
|
+
} else {
|
|
504
|
+
const learnCard = await connect(project, __spreadProps$7(__spreadValues$9({}, options), { lca: true }));
|
|
505
|
+
await ensureProfile(learnCard, identity, project);
|
|
506
|
+
authority = await setupSigning(project, learnCard, options.name);
|
|
507
|
+
}
|
|
508
|
+
out.set({
|
|
509
|
+
profileId: identity.profileId,
|
|
510
|
+
signingAuthority: {
|
|
511
|
+
name: authority.name,
|
|
512
|
+
endpoint: authority.endpoint,
|
|
513
|
+
did: authority.did
|
|
514
|
+
},
|
|
515
|
+
alreadyConfigured: authority.alreadyConfigured
|
|
516
|
+
});
|
|
517
|
+
out.log(
|
|
518
|
+
"Send from a template: npx @learncard/cli send you@example.com --template\nSee it in the app: npx @learncard/cli open"
|
|
519
|
+
);
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
var setupSigning$1 = /*#__PURE__*/Object.freeze({
|
|
523
|
+
__proto__: null,
|
|
524
|
+
authorityName: authorityName,
|
|
525
|
+
setupSigning: setupSigning,
|
|
526
|
+
runSetupSigning: runSetupSigning
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
const SEND_MJS = "import { randomUUID } from 'node:crypto';\nimport { initLearnCard } from '@learncard/init';\n\nconst recipientEmail = process.argv[2];\nif (!recipientEmail) throw new Error('Usage: node --env-file=.env send.mjs you@example.com');\n\n// `network: true` connects to the production LearnCard Network.\nconst learnCard = await initLearnCard({ seed: process.env.SECURE_SEED, network: true });\n\n// Your public identity on the network. Created once; safe to re-run.\nif (!(await learnCard.invoke.getProfile())) {\n await learnCard.invoke.createProfile({\n profileId: process.env.PROFILE_ID,\n displayName: 'My Organization',\n });\n}\n\n// A minimal Open Badges 3.0 credential, signed by you.\nconst credential = await learnCard.invoke.issueCredential({\n '@context': [\n 'https://www.w3.org/ns/credentials/v2',\n 'https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json',\n ],\n type: ['VerifiableCredential', 'OpenBadgeCredential'],\n issuer: learnCard.id.did(),\n validFrom: new Date().toISOString(),\n name: 'Quickstart Complete',\n credentialSubject: {\n type: ['AchievementSubject'],\n achievement: {\n id: `urn:uuid:${randomUUID()}`,\n type: ['Achievement'],\n name: 'Quickstart Complete',\n description: 'Sent a verifiable credential with LearnCard.',\n criteria: { narrative: 'Ran the LearnCard quickstart.' },\n },\n },\n});\n\n// Send it. The recipient can be an email, phone number, profile ID, or DID.\nconst result = await learnCard.invoke.send({\n type: 'boost',\n recipient: recipientEmail,\n signedCredential: credential,\n});\n\nif (result.inbox?.status === 'PENDING') {\n console.log(\n `Sent. ${recipientEmail} will get a claim email. You can also share this link directly:\\n${result.inbox.claimUrl}`\n );\n} else {\n console.log(\n `Delivered. ${recipientEmail} already uses LearnCard \u2014 the credential is in their wallet.`\n );\n}\nconsole.log(`Reusable template for this badge: ${result.uri}`);\n";
|
|
530
|
+
const SEND_SH = 'curl -X POST https://network.learncard.com/api/send \\\n -H "Authorization: Bearer $TOKEN" \\\n -H "Content-Type: application/json" \\\n -d @request.json\n';
|
|
531
|
+
const SEND_FROM_TEMPLATE_MJS = "import { initLearnCard } from '@learncard/init';\n\nconst recipient = process.argv[2];\nif (!recipient)\n throw new Error('Usage: node --env-file=.env send-from-template.mjs you@example.com');\nif (!process.env.TEMPLATE_URI)\n throw new Error('Run npx @learncard/cli send you@example.com --template first.');\n\n// The CLI saved a template and registered your primary signing authority once.\nconst learnCard = await initLearnCard({ seed: process.env.SECURE_SEED, network: true });\nconst result = await learnCard.invoke.send({\n type: 'boost',\n recipient,\n templateUri: process.env.TEMPLATE_URI,\n});\nconsole.log(\n result.inbox?.status === 'PENDING'\n ? `Sent. ${recipient} will get a claim email. You can also share this link directly:\\n${result.inbox.claimUrl}`\n : `Delivered. ${recipient} already uses LearnCard \u2014 the credential is in their wallet.`\n);\nconsole.log(`Reusable template for this badge: ${result.uri}`);\n";
|
|
532
|
+
const WEBHOOK_MJS = "import { createServer } from 'node:http';\nimport { pathToFileURL } from 'node:url';\n\nexport const extractBearer = header =>\n typeof header === 'string' ? /^Bearer\\s+(\\S+)$/i.exec(header)?.[1] : undefined;\n\nexport const webhookDedupeKey = payload => {\n const id = payload?.data?.inbox?.issuanceId;\n return ['ISSUANCE_DELIVERED', 'ISSUANCE_CLAIMED', 'ISSUANCE_ERROR'].includes(payload?.type) &&\n typeof id === 'string' &&\n id.length > 0\n ? `${payload.type}:${id}`\n : undefined;\n};\n\nexport const createWebhookReceiver = (verifier, expectedDid = process.env.EXPECTED_NETWORK_DID) => {\n // Demo only: bounded, in-memory deduplication. Use durable storage in production.\n const seen = new Set();\n return createServer(async (req, res) => {\n if (req.method !== 'POST') return void res.writeHead(404).end();\n const token = extractBearer(req.headers.authorization);\n try {\n if (!token) return void res.writeHead(401).end();\n const result = await verifier.invoke.verifyPresentation(token, { proofFormat: 'jwt' });\n if (result.errors.length) return void res.writeHead(401).end();\n const claims = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString());\n if (expectedDid && claims.iss !== expectedDid) return void res.writeHead(403).end();\n } catch {\n return void res.writeHead(401).end();\n }\n try {\n const chunks = [];\n let size = 0;\n for await (const chunk of req) {\n size += chunk.length;\n if (size > 65536) return void res.writeHead(413).end();\n chunks.push(chunk);\n }\n const payload = JSON.parse(Buffer.concat(chunks).toString('utf8'));\n const key = webhookDedupeKey(payload);\n if (!key) return void res.writeHead(400).end();\n // Acknowledge before doing any application work; LearnCard waits six seconds.\n res.writeHead(200).end();\n if (seen.has(key)) return;\n if (seen.size >= 10000) seen.delete(seen.values().next().value);\n seen.add(key);\n const inbox = payload.data.inbox;\n const fields = [\n payload.type,\n inbox.status,\n inbox.issuanceId,\n inbox.recipient?.learnCardId,\n ];\n console.log(\n fields\n .map(value =>\n typeof value === 'string'\n ? value.replace(/[\\u0000-\\u001f\\u007f-\\u009f\\u2028\\u2029]/g, '?')\n : ''\n )\n .join(' ')\n .trim()\n );\n } catch {\n if (!res.headersSent) res.writeHead(400).end();\n }\n });\n};\n\nif (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {\n const { initLearnCard } = await import('@learncard/init');\n const port = Number(process.env.PORT || 8787);\n if (!Number.isInteger(port) || port < 1 || port > 65535)\n throw new Error('PORT must be 1\u201365535');\n const verifier = await initLearnCard();\n if (!process.env.EXPECTED_NETWORK_DID) {\n console.log(\n 'Demo: signatures are verified, but any DID is accepted. Set EXPECTED_NETWORK_DID to your trusted network DID before production.'\n );\n }\n const server = createWebhookReceiver(verifier);\n server.requestTimeout = 5000;\n server.listen(port, () => console.log(`Listening on http://localhost:${port}`));\n}\n";
|
|
533
|
+
const CLAIM_BUTTON_HTML = `<!doctype html>
|
|
534
|
+
<html lang="en">
|
|
535
|
+
<head>
|
|
536
|
+
<meta charset="utf-8" />
|
|
537
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
538
|
+
<title>Claim your badge</title>
|
|
539
|
+
</head>
|
|
540
|
+
<body>
|
|
541
|
+
<h1>Claim your badge</h1>
|
|
542
|
+
<div id="claim-button"></div>
|
|
543
|
+
<script src="https://cdn.jsdelivr.net/npm/@learncard/embed-sdk@latest/dist/learncard.js"><\/script>
|
|
544
|
+
<script>
|
|
545
|
+
// init renders the Claim button into #claim-button.
|
|
546
|
+
LearnCard.init({
|
|
547
|
+
target: '#claim-button',
|
|
548
|
+
publishableKey: 'PUBLISHABLE_KEY_PLACEHOLDER',
|
|
549
|
+
apiBaseUrl: 'https://network.learncard.com/api',
|
|
550
|
+
// A full unsigned badge works without a named integration template.
|
|
551
|
+
credential: {
|
|
552
|
+
'@context': [
|
|
553
|
+
'https://www.w3.org/ns/credentials/v2',
|
|
554
|
+
'https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json',
|
|
555
|
+
'https://ctx.learncard.com/boosts/1.0.3.json',
|
|
556
|
+
],
|
|
557
|
+
type: ['VerifiableCredential', 'OpenBadgeCredential', 'BoostCredential'],
|
|
558
|
+
name: 'Badge Name',
|
|
559
|
+
credentialSubject: {
|
|
560
|
+
type: ['AchievementSubject'],
|
|
561
|
+
achievement: {
|
|
562
|
+
id: 'urn:uuid:552bf83b-7700-4c3a-b1ce-2d8f8ee68811',
|
|
563
|
+
type: ['Achievement'],
|
|
564
|
+
name: 'Badge Name',
|
|
565
|
+
description: 'Claimed a badge with LearnCard.',
|
|
566
|
+
criteria: { narrative: 'Clicked the Claim button.' },
|
|
567
|
+
},
|
|
568
|
+
},
|
|
569
|
+
},
|
|
570
|
+
});
|
|
571
|
+
<\/script>
|
|
572
|
+
</body>
|
|
573
|
+
</html>
|
|
574
|
+
`;
|
|
575
|
+
const CREATE_CONTRACT_MJS = "import { initLearnCard } from '@learncard/init';\n\nconst { SECURE_SEED, PROFILE_ID, RETURN_TO } = process.env;\nif (!SECURE_SEED || !PROFILE_ID || !RETURN_TO) {\n throw new Error('Set SECURE_SEED, PROFILE_ID, and RETURN_TO');\n}\nconst returnTo = new URL(RETURN_TO);\nif (returnTo.protocol !== 'https:') throw new Error('RETURN_TO must use HTTPS');\n\nconst learnCard = await initLearnCard({ seed: SECURE_SEED, network: true });\nif (!(await learnCard.invoke.getProfile())) {\n await learnCard.invoke.createServiceProfile({\n profileId: PROFILE_ID,\n displayName: 'ConsentFlow Tutorial',\n });\n}\n\nconst contractUri = await learnCard.invoke.createContract({\n name: 'ConsentFlow Tutorial',\n subtitle: 'Receive a tutorial badge',\n description: 'Allow us to read your name and send an achievement.',\n redirectUrl: returnTo.toString(),\n contract: {\n read: {\n personal: { name: { required: false } },\n credentials: { categories: { Achievement: { required: false } } },\n },\n write: {\n personal: {},\n credentials: { categories: { Achievement: { required: true } } },\n },\n },\n});\n\nconst consentUrl = new URL('https://learncard.app/consent-flow');\nconsentUrl.searchParams.set('uri', contractUri);\nconsentUrl.searchParams.set('returnTo', returnTo.toString());\nconsole.log(JSON.stringify({ contractUri, consentUrl: consentUrl.toString() }));\n";
|
|
576
|
+
const CONSENT_CALLBACK_MJS = "import { createServer } from 'node:http';\nimport { pathToFileURL } from 'node:url';\nimport { initLearnCard } from '@learncard/init';\n\n// Decoding is not verification: inspect claims only after verifying the signature.\nexport const verifyConsentRedirect = async (learnCard, vp, contractUri) => {\n if (!vp) return { status: 'denied-or-abandoned' };\n if (typeof vp !== 'string' || vp.split('.').length !== 3 || !contractUri) {\n throw new Error('Invalid consent proof');\n }\n const result = await learnCard.invoke.verifyPresentation(vp, { proofFormat: 'jwt' });\n if (result.errors.length !== 0) throw new Error('Invalid consent proof');\n\n const payload = JSON.parse(Buffer.from(vp.split('.')[1], 'base64url').toString('utf8'));\n const userDid = payload.vp?.holder;\n if (typeof userDid !== 'string' || !userDid.startsWith('did:') || payload.iss !== userDid) {\n throw new Error('Invalid consent holder');\n }\n if (payload.vp.contractUri !== contractUri) throw new Error('Wrong consent contract');\n return { status: 'verified', userDid };\n};\n\nexport const createConsentCallback = (learnCard, contractUri) => async (req, res) => {\n res.setHeader('Cache-Control', 'no-store');\n res.setHeader('Referrer-Policy', 'no-referrer');\n res.setHeader('Content-Type', 'text/plain; charset=utf-8');\n const url = new URL(req.url, 'http://localhost');\n if (req.method !== 'GET' || url.pathname !== '/consent-callback') {\n res.writeHead(404).end('Not found');\n return;\n }\n try {\n const verified = await verifyConsentRedirect(\n learnCard,\n url.searchParams.get('vp'),\n contractUri\n );\n // The convenience `did` parameter is deliberately never read.\n if (verified.status !== 'verified') {\n res.writeHead(400).end('Consent denied or abandoned. Try again.');\n return;\n }\n const { readUserData } = await import('./read-user-data.mjs');\n const records = await readUserData(learnCard, verified.userDid, contractUri);\n if (records.length === 0) {\n res.writeHead(403).end('No active consent.');\n return;\n }\n // Do not expose personal data, create a login session, or issue on this GET.\n res.end('Consent verified. Active access confirmed.');\n } catch {\n res.writeHead(400).end('Unable to confirm consent. Try again.');\n }\n};\n\nif (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {\n const { SECURE_SEED, CONTRACT_URI } = process.env;\n if (!SECURE_SEED || !CONTRACT_URI) throw new Error('Set SECURE_SEED and CONTRACT_URI');\n const learnCard = await initLearnCard({ seed: SECURE_SEED, network: true });\n createServer(createConsentCallback(learnCard, CONTRACT_URI)).listen(3000, '127.0.0.1');\n console.log('Callback listening on port 3000.');\n}\n";
|
|
577
|
+
const READ_USER_DATA_MJS = "import { pathToFileURL } from 'node:url';\nimport { initLearnCard } from '@learncard/init';\nimport { verifyConsentRedirect } from './consent-callback.mjs';\n\nexport const readUserData = async (learnCard, userDid, contractUri) => {\n const profile = await learnCard.invoke.getProfile(userDid);\n if (!profile) return [];\n const hasAccess = () => learnCard.invoke.verifyConsent(contractUri, profile.profileId);\n if (!(await hasAccess())) return [];\n const records = [];\n let cursor;\n do {\n const page = await learnCard.invoke.getConsentFlowDataForDid(userDid, {\n limit: 100,\n ...(cursor ? { cursor } : {}),\n });\n records.push(...page.records.filter(record => record.contractUri === contractUri));\n if (!page.hasMore) return (await hasAccess()) ? records : [];\n if (!page.cursor || page.cursor === cursor) throw new Error('Pagination did not advance');\n cursor = page.cursor;\n } while (true);\n};\n\nif (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {\n const { SECURE_SEED, CONTRACT_URI, CONSENT_VP } = process.env;\n if (!SECURE_SEED || !CONTRACT_URI) throw new Error('Set SECURE_SEED and CONTRACT_URI');\n const learnCard = await initLearnCard({ seed: SECURE_SEED, network: true });\n const verified = await verifyConsentRedirect(learnCard, CONSENT_VP, CONTRACT_URI);\n if (verified.status !== 'verified') throw new Error('Consent denied or abandoned');\n const records = await readUserData(learnCard, verified.userDid, CONTRACT_URI);\n console.log(\n records.length ? `Active consent: ${records.length} record(s).` : 'No active consent.'\n );\n}\n";
|
|
578
|
+
const ISSUE_THROUGH_CONTRACT_MJS = "import { pathToFileURL } from 'node:url';\nimport { initLearnCard } from '@learncard/init';\nimport { verifyConsentRedirect } from './consent-callback.mjs';\nimport { readUserData } from './read-user-data.mjs';\n\nexport const issueThroughContract = async (learnCard, userDid, contractUri) => {\n if ((await readUserData(learnCard, userDid, contractUri)).length === 0) {\n throw new Error('No active consent');\n }\n const base = learnCard.invoke.newCredential({ type: 'boost' });\n const unsignedCredential = {\n ...base,\n issuer: learnCard.id.did(),\n name: 'ConsentFlow Tutorial Complete',\n credentialSubject: {\n ...base.credentialSubject,\n id: userDid,\n achievement: {\n ...base.credentialSubject.achievement,\n name: 'ConsentFlow Tutorial Complete',\n description: 'Completed the ConsentFlow tutorial.',\n },\n },\n };\n const boostUri = await learnCard.invoke.createBoost(unsignedCredential, {\n name: 'ConsentFlow Tutorial Complete',\n category: 'Achievement',\n type: 'achievement',\n });\n const signedCredential = await learnCard.invoke.issueCredential({\n ...unsignedCredential,\n boostId: boostUri,\n });\n return learnCard.invoke.writeCredentialToContract(\n userDid,\n contractUri,\n signedCredential,\n boostUri\n );\n};\n\nif (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {\n const { SECURE_SEED, CONTRACT_URI, CONSENT_VP } = process.env;\n if (!SECURE_SEED || !CONTRACT_URI) throw new Error('Set SECURE_SEED and CONTRACT_URI');\n const learnCard = await initLearnCard({ seed: SECURE_SEED, network: true });\n const verified = await verifyConsentRedirect(learnCard, CONSENT_VP, CONTRACT_URI);\n if (verified.status !== 'verified') throw new Error('Consent denied or abandoned');\n const credentialUri = await issueThroughContract(learnCard, verified.userDid, CONTRACT_URI);\n console.log(`Issued: ${credentialUri}`);\n}\n";
|
|
579
|
+
|
|
580
|
+
var __defProp$8 = Object.defineProperty;
|
|
581
|
+
var __defProps$6 = Object.defineProperties;
|
|
582
|
+
var __getOwnPropDescs$6 = Object.getOwnPropertyDescriptors;
|
|
583
|
+
var __getOwnPropSymbols$8 = Object.getOwnPropertySymbols;
|
|
584
|
+
var __hasOwnProp$8 = Object.prototype.hasOwnProperty;
|
|
585
|
+
var __propIsEnum$8 = Object.prototype.propertyIsEnumerable;
|
|
586
|
+
var __defNormalProp$8 = (obj, key, value) => key in obj ? __defProp$8(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
587
|
+
var __spreadValues$8 = (a, b) => {
|
|
588
|
+
for (var prop in b || (b = {}))
|
|
589
|
+
if (__hasOwnProp$8.call(b, prop))
|
|
590
|
+
__defNormalProp$8(a, prop, b[prop]);
|
|
591
|
+
if (__getOwnPropSymbols$8)
|
|
592
|
+
for (var prop of __getOwnPropSymbols$8(b)) {
|
|
593
|
+
if (__propIsEnum$8.call(b, prop))
|
|
594
|
+
__defNormalProp$8(a, prop, b[prop]);
|
|
595
|
+
}
|
|
596
|
+
return a;
|
|
597
|
+
};
|
|
598
|
+
var __spreadProps$6 = (a, b) => __defProps$6(a, __getOwnPropDescs$6(b));
|
|
599
|
+
const DEFAULT_BADGE = {
|
|
600
|
+
name: "Quickstart Complete",
|
|
601
|
+
description: "Sent a verifiable credential with LearnCard."
|
|
602
|
+
};
|
|
603
|
+
const quickstartCredential = (issuerDid, badge = DEFAULT_BADGE) => ({
|
|
604
|
+
"@context": [
|
|
605
|
+
"https://www.w3.org/ns/credentials/v2",
|
|
606
|
+
"https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json"
|
|
607
|
+
],
|
|
608
|
+
type: ["VerifiableCredential", "OpenBadgeCredential"],
|
|
609
|
+
issuer: issuerDid,
|
|
610
|
+
validFrom: (/* @__PURE__ */ new Date()).toISOString(),
|
|
611
|
+
name: badge.name,
|
|
612
|
+
credentialSubject: {
|
|
613
|
+
type: ["AchievementSubject"],
|
|
614
|
+
achievement: {
|
|
615
|
+
id: `urn:uuid:${node_crypto.randomUUID()}`,
|
|
616
|
+
type: ["Achievement"],
|
|
617
|
+
name: badge.name,
|
|
618
|
+
description: badge.description,
|
|
619
|
+
criteria: { narrative: "Ran the LearnCard quickstart." }
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
const templateCredential = (issuerDid, badge = DEFAULT_BADGE) => {
|
|
624
|
+
const credential = quickstartCredential(issuerDid, badge);
|
|
625
|
+
credential["@context"].push("https://ctx.learncard.com/boosts/1.0.1.json");
|
|
626
|
+
credential.type.push("BoostCredential");
|
|
627
|
+
return credential;
|
|
628
|
+
};
|
|
629
|
+
const sendOptions = (options) => {
|
|
630
|
+
const picked = {
|
|
631
|
+
webhookUrl: options.webhookUrl,
|
|
632
|
+
suppressDelivery: options.suppressDelivery,
|
|
633
|
+
guardianEmail: options.guardianEmail
|
|
634
|
+
};
|
|
635
|
+
return Object.values(picked).some((v) => v !== void 0) ? { options: picked } : {};
|
|
636
|
+
};
|
|
637
|
+
const withDeliveryOptions = (content, anchor, deliveryOptions) => deliveryOptions ? content.replace(
|
|
638
|
+
anchor,
|
|
639
|
+
() => `${anchor}
|
|
640
|
+
options: ${JSON.stringify(deliveryOptions)},`
|
|
641
|
+
) : content;
|
|
642
|
+
const personalizeSendMjs = (displayName, badge, deliveryOptions) => withDeliveryOptions(
|
|
643
|
+
SEND_MJS.replace(
|
|
644
|
+
"displayName: 'My Organization'",
|
|
645
|
+
() => `displayName: ${JSON.stringify(displayName)}`
|
|
646
|
+
).split("'Quickstart Complete'").join(JSON.stringify(badge.name)).replace(
|
|
647
|
+
"'Sent a verifiable credential with LearnCard.'",
|
|
648
|
+
() => JSON.stringify(badge.description)
|
|
649
|
+
),
|
|
650
|
+
" signedCredential: credential,",
|
|
651
|
+
deliveryOptions
|
|
652
|
+
);
|
|
653
|
+
const personalizeSendFromTemplateMjs = (deliveryOptions) => withDeliveryOptions(
|
|
654
|
+
SEND_FROM_TEMPLATE_MJS,
|
|
655
|
+
" templateUri: process.env.TEMPLATE_URI,",
|
|
656
|
+
deliveryOptions
|
|
657
|
+
);
|
|
658
|
+
const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
659
|
+
const PHONE = /^\+?\d{10,15}$/;
|
|
660
|
+
const runSend = async (recipientEmail, options) => {
|
|
661
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
662
|
+
if (!EMAIL.test(recipientEmail) && !PHONE.test(recipientEmail))
|
|
663
|
+
throw new Error(
|
|
664
|
+
`"${recipientEmail}" is not an email address or phone number. Example: npx @learncard/cli send you@example.com`
|
|
665
|
+
);
|
|
666
|
+
const cwd = process.cwd();
|
|
667
|
+
const project = await loadProject(cwd);
|
|
668
|
+
const prompts = createPrompts(options.yes);
|
|
669
|
+
let displayName;
|
|
670
|
+
let badge;
|
|
671
|
+
try {
|
|
672
|
+
const needsName = !project.env.PROFILE_ID && !options.profileId && !options.name;
|
|
673
|
+
displayName = needsName ? await prompts.ask("Display name for your issuer profile", "My Organization") : options.name;
|
|
674
|
+
badge = {
|
|
675
|
+
name: (_a = options.badge) != null ? _a : await prompts.ask("Badge name", DEFAULT_BADGE.name),
|
|
676
|
+
description: (_b = options.description) != null ? _b : DEFAULT_BADGE.description
|
|
677
|
+
};
|
|
678
|
+
} finally {
|
|
679
|
+
prompts.close();
|
|
680
|
+
}
|
|
681
|
+
const identity = await ensureIdentity(project, __spreadProps$6(__spreadValues$8({}, options), { name: displayName, yes: true }));
|
|
682
|
+
const useTemplate = options.template || !!options.templateUri;
|
|
683
|
+
const learnCard = useTemplate ? await connect(project, __spreadProps$6(__spreadValues$8({}, options), { lca: true })) : await connect(project, options);
|
|
684
|
+
await ensureProfile(learnCard, identity, project);
|
|
685
|
+
const effectiveSendOptions = sendOptions(options);
|
|
686
|
+
let result;
|
|
687
|
+
if (useTemplate) {
|
|
688
|
+
await setupSigning(
|
|
689
|
+
project,
|
|
690
|
+
learnCard
|
|
691
|
+
);
|
|
692
|
+
if (options.templateUri) {
|
|
693
|
+
out.log(`Sending from template ${options.templateUri}.`);
|
|
694
|
+
if (!project.env.TEMPLATE_URI) {
|
|
695
|
+
await saveProject(project, { TEMPLATE_URI: options.templateUri });
|
|
696
|
+
}
|
|
697
|
+
} else if (!project.env.TEMPLATE_URI) {
|
|
698
|
+
const uri = await learnCard.invoke.createBoost(
|
|
699
|
+
templateCredential(learnCard.id.did(), badge),
|
|
700
|
+
{
|
|
701
|
+
name: badge.name,
|
|
702
|
+
category: "Achievement",
|
|
703
|
+
status: "LIVE"
|
|
704
|
+
}
|
|
705
|
+
);
|
|
706
|
+
await saveProject(project, { TEMPLATE_URI: uri });
|
|
707
|
+
} else {
|
|
708
|
+
out.log(
|
|
709
|
+
`Reusing template ${project.env.TEMPLATE_URI}; its saved badge name and description are unchanged.`
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
result = await learnCard.invoke.send(__spreadValues$8({
|
|
713
|
+
type: "boost",
|
|
714
|
+
recipient: recipientEmail,
|
|
715
|
+
templateUri: (_c = options.templateUri) != null ? _c : project.env.TEMPLATE_URI
|
|
716
|
+
}, effectiveSendOptions));
|
|
717
|
+
} else {
|
|
718
|
+
const credential = await learnCard.invoke.issueCredential(
|
|
719
|
+
quickstartCredential(learnCard.id.did(), badge)
|
|
720
|
+
);
|
|
721
|
+
result = await learnCard.invoke.send(__spreadValues$8({
|
|
722
|
+
type: "boost",
|
|
723
|
+
recipient: recipientEmail,
|
|
724
|
+
signedCredential: credential
|
|
725
|
+
}, effectiveSendOptions));
|
|
726
|
+
}
|
|
727
|
+
out.log("");
|
|
728
|
+
if (((_d = result.inbox) == null ? void 0 : _d.status) === "PENDING") {
|
|
729
|
+
out.log(
|
|
730
|
+
`Sent. ${recipientEmail} will get a claim email. You can also share this link directly:
|
|
731
|
+
${result.inbox.claimUrl}`
|
|
732
|
+
);
|
|
733
|
+
} else {
|
|
734
|
+
out.log(
|
|
735
|
+
`Delivered. ${recipientEmail} already uses LearnCard \u2014 the credential is in their wallet.`
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
out.log(`Reusable template for this badge: ${result.uri}`);
|
|
739
|
+
const filename = useTemplate ? "send-from-template.mjs" : "send.mjs";
|
|
740
|
+
const sendPath = path__default$1["default"].join(cwd, filename);
|
|
741
|
+
let wroteSendFile = false;
|
|
742
|
+
if (!await fs__default$1["default"].stat(sendPath).catch(() => null)) {
|
|
743
|
+
await fs__default$1["default"].writeFile(
|
|
744
|
+
sendPath,
|
|
745
|
+
localizeSnippet(
|
|
746
|
+
useTemplate ? personalizeSendFromTemplateMjs(effectiveSendOptions.options) : personalizeSendMjs(identity.displayName, badge, effectiveSendOptions.options),
|
|
747
|
+
resolveServices(project.env, options.network)
|
|
748
|
+
)
|
|
749
|
+
);
|
|
750
|
+
wroteSendFile = true;
|
|
751
|
+
out.log(
|
|
752
|
+
`
|
|
753
|
+
The code that just ran is in ./${filename} \u2014 run it yourself:
|
|
754
|
+
npm install @learncard/init
|
|
755
|
+
node --env-file=.env ${filename} ${recipientEmail}`
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
out.log(`Check whether it was claimed: npx @learncard/cli status ${result.activityId}`);
|
|
759
|
+
out.log(`See it in the app: npx @learncard/cli open${options.template ? " template" : ""}`);
|
|
760
|
+
out.set(__spreadProps$6(__spreadValues$8(__spreadValues$8(__spreadProps$6(__spreadValues$8({
|
|
761
|
+
profileId: identity.profileId,
|
|
762
|
+
did: learnCard.id.did(),
|
|
763
|
+
recipient: recipientEmail,
|
|
764
|
+
status: ((_e = result.inbox) == null ? void 0 : _e.status) === "PENDING" ? "PENDING" : "ISSUED"
|
|
765
|
+
}, ((_f = result.inbox) == null ? void 0 : _f.claimUrl) && { claimUrl: result.inbox.claimUrl }), {
|
|
766
|
+
templateUri: result.uri,
|
|
767
|
+
activityId: result.activityId
|
|
768
|
+
}), result.credentialUri && { credentialUri: result.credentialUri }), ((_g = result.inbox) == null ? void 0 : _g.issuanceId) && { issuanceId: result.inbox.issuanceId }), {
|
|
769
|
+
files: wroteSendFile ? [`./${filename}`] : []
|
|
770
|
+
}));
|
|
771
|
+
};
|
|
772
|
+
|
|
773
|
+
var __defProp$7 = Object.defineProperty;
|
|
774
|
+
var __defProps$5 = Object.defineProperties;
|
|
775
|
+
var __getOwnPropDescs$5 = Object.getOwnPropertyDescriptors;
|
|
776
|
+
var __getOwnPropSymbols$7 = Object.getOwnPropertySymbols;
|
|
777
|
+
var __hasOwnProp$7 = Object.prototype.hasOwnProperty;
|
|
778
|
+
var __propIsEnum$7 = Object.prototype.propertyIsEnumerable;
|
|
779
|
+
var __defNormalProp$7 = (obj, key, value) => key in obj ? __defProp$7(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
780
|
+
var __spreadValues$7 = (a, b) => {
|
|
781
|
+
for (var prop in b || (b = {}))
|
|
782
|
+
if (__hasOwnProp$7.call(b, prop))
|
|
783
|
+
__defNormalProp$7(a, prop, b[prop]);
|
|
784
|
+
if (__getOwnPropSymbols$7)
|
|
785
|
+
for (var prop of __getOwnPropSymbols$7(b)) {
|
|
786
|
+
if (__propIsEnum$7.call(b, prop))
|
|
787
|
+
__defNormalProp$7(a, prop, b[prop]);
|
|
788
|
+
}
|
|
789
|
+
return a;
|
|
790
|
+
};
|
|
791
|
+
var __spreadProps$5 = (a, b) => __defProps$5(a, __getOwnPropDescs$5(b));
|
|
73
792
|
const createExportLearnCardBundleHelper = (exportBundle, defaultWallet) => {
|
|
74
793
|
return (walletOrOptions, maybeOptions) => {
|
|
75
794
|
if (maybeOptions)
|
|
@@ -80,25 +799,25 @@ const createExportLearnCardBundleHelper = (exportBundle, defaultWallet) => {
|
|
|
80
799
|
const createRestoreLearnCardFromBundleHelper = (restoreBundle, defaultInit) => {
|
|
81
800
|
return (path, options = {}) => {
|
|
82
801
|
var _a;
|
|
83
|
-
return restoreBundle(path, __spreadProps(__spreadValues({}, options), {
|
|
84
|
-
init: __spreadValues(__spreadValues({}, defaultInit), (_a = options.init) != null ? _a : {})
|
|
802
|
+
return restoreBundle(path, __spreadProps$5(__spreadValues$7({}, options), {
|
|
803
|
+
init: __spreadValues$7(__spreadValues$7({}, defaultInit), (_a = options.init) != null ? _a : {})
|
|
85
804
|
}));
|
|
86
805
|
};
|
|
87
806
|
};
|
|
88
807
|
|
|
89
808
|
var name = "@learncard/cli";
|
|
90
|
-
var version = "3.
|
|
809
|
+
var version = "3.5.0";
|
|
91
810
|
var description = "Command Line Interface for LearnCard";
|
|
92
811
|
var main = "dist/index.js";
|
|
93
812
|
var bin = "dist/index.js";
|
|
94
813
|
var scripts = {
|
|
95
|
-
start: "bun --conditions=development src/index.tsx",
|
|
96
|
-
"start:built": "
|
|
97
|
-
dev: "bun
|
|
98
|
-
build: "rollup -c",
|
|
99
|
-
test: "vitest run",
|
|
100
|
-
"test:watch": "vitest",
|
|
101
|
-
"test:coverage": "vitest run --coverage"
|
|
814
|
+
start: "bun scripts/embed-snippets.ts && bun --conditions=development src/index.tsx",
|
|
815
|
+
"start:built": "bun run build && node --enable-source-maps dist/index.js",
|
|
816
|
+
dev: "bun run start",
|
|
817
|
+
build: "bun scripts/embed-snippets.ts && rollup -c",
|
|
818
|
+
test: "bun scripts/embed-snippets.ts && vitest run",
|
|
819
|
+
"test:watch": "bun scripts/embed-snippets.ts && vitest",
|
|
820
|
+
"test:coverage": "bun scripts/embed-snippets.ts && vitest run --coverage"
|
|
102
821
|
};
|
|
103
822
|
var author = "Learning Economy Foundation (www.learningeconomy.io)";
|
|
104
823
|
var license = "MIT";
|
|
@@ -114,14 +833,11 @@ var dependencies = {
|
|
|
114
833
|
"@learncard/render-method-plugin": "workspace:*",
|
|
115
834
|
"@learncard/holder-continuity": "workspace:*",
|
|
116
835
|
"@learncard/types": "workspace:*",
|
|
117
|
-
"@rollup/plugin-json": "^4.1.0",
|
|
118
836
|
clipboardy: "^4.0.0",
|
|
119
837
|
commander: "^9.3.0",
|
|
120
838
|
figlet: "^1.5.2",
|
|
121
839
|
"gradient-string": "^2.0.1",
|
|
122
|
-
"pretty-repl": "^3.1.1"
|
|
123
|
-
rollup: "^2.80.0",
|
|
124
|
-
"rollup-plugin-esbuild": "^4.9.1"
|
|
840
|
+
"pretty-repl": "^3.1.1"
|
|
125
841
|
};
|
|
126
842
|
var devDependencies = {
|
|
127
843
|
"@types/cors": "^2.8.12",
|
|
@@ -129,7 +845,10 @@ var devDependencies = {
|
|
|
129
845
|
"@types/node": "^18.0.0",
|
|
130
846
|
nodemon: "^2.0.16",
|
|
131
847
|
"ts-node": "^10.8.1",
|
|
132
|
-
vitest: "4.1.10"
|
|
848
|
+
vitest: "4.1.10",
|
|
849
|
+
"@rollup/plugin-json": "^4.1.0",
|
|
850
|
+
rollup: "^2.80.0",
|
|
851
|
+
"rollup-plugin-esbuild": "^4.9.1"
|
|
133
852
|
};
|
|
134
853
|
var repository = {
|
|
135
854
|
type: "git",
|
|
@@ -155,8 +874,27 @@ var packageJson = {
|
|
|
155
874
|
bugs: bugs
|
|
156
875
|
};
|
|
157
876
|
|
|
158
|
-
var
|
|
159
|
-
var
|
|
877
|
+
var __defProp$6 = Object.defineProperty;
|
|
878
|
+
var __defProps$4 = Object.defineProperties;
|
|
879
|
+
var __getOwnPropDescs$4 = Object.getOwnPropertyDescriptors;
|
|
880
|
+
var __getOwnPropSymbols$6 = Object.getOwnPropertySymbols;
|
|
881
|
+
var __hasOwnProp$6 = Object.prototype.hasOwnProperty;
|
|
882
|
+
var __propIsEnum$6 = Object.prototype.propertyIsEnumerable;
|
|
883
|
+
var __knownSymbol$1 = (name, symbol) => (symbol = Symbol[name]) ? symbol : /* @__PURE__ */ Symbol.for("Symbol." + name);
|
|
884
|
+
var __defNormalProp$6 = (obj, key, value) => key in obj ? __defProp$6(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
885
|
+
var __spreadValues$6 = (a, b) => {
|
|
886
|
+
for (var prop in b || (b = {}))
|
|
887
|
+
if (__hasOwnProp$6.call(b, prop))
|
|
888
|
+
__defNormalProp$6(a, prop, b[prop]);
|
|
889
|
+
if (__getOwnPropSymbols$6)
|
|
890
|
+
for (var prop of __getOwnPropSymbols$6(b)) {
|
|
891
|
+
if (__propIsEnum$6.call(b, prop))
|
|
892
|
+
__defNormalProp$6(a, prop, b[prop]);
|
|
893
|
+
}
|
|
894
|
+
return a;
|
|
895
|
+
};
|
|
896
|
+
var __spreadProps$4 = (a, b) => __defProps$4(a, __getOwnPropDescs$4(b));
|
|
897
|
+
var __forAwait$1 = (obj, it, method) => (it = obj[__knownSymbol$1("asyncIterator")]) ? it.call(obj) : (obj = obj[__knownSymbol$1("iterator")](), it = {}, method = (key, fn) => (fn = obj[key]) && (it[key] = (arg) => new Promise((yes, no, done) => (arg = fn.call(obj, arg), done = arg.done, Promise.resolve(arg.value).then((value) => yes({ value, done }), no)))), method("next"), method("return"), it);
|
|
160
898
|
dns__default["default"].setDefaultResultOrder("ipv4first");
|
|
161
899
|
const cliGlobals = globalThis;
|
|
162
900
|
const replGlobals = [
|
|
@@ -328,7 +1066,7 @@ const startReadlineRepl = async (colorize) => {
|
|
|
328
1066
|
});
|
|
329
1067
|
rl.prompt();
|
|
330
1068
|
try {
|
|
331
|
-
for (var iter = __forAwait(rl), more, temp, error; more = !(temp = await iter.next()).done; more = false) {
|
|
1069
|
+
for (var iter = __forAwait$1(rl), more, temp, error; more = !(temp = await iter.next()).done; more = false) {
|
|
332
1070
|
const line = temp.value;
|
|
333
1071
|
const source = line.trim();
|
|
334
1072
|
if (source === ".exit") break;
|
|
@@ -373,14 +1111,199 @@ const startCliRepl = async (colorize) => {
|
|
|
373
1111
|
}
|
|
374
1112
|
await startBunRepl(colorize);
|
|
375
1113
|
};
|
|
376
|
-
commander.program.
|
|
1114
|
+
commander.program.command("send <email>").description(
|
|
1115
|
+
'Send a "Quickstart Complete" badge to an email address. Creates .env and send.mjs in the current folder.'
|
|
1116
|
+
).option("-y, --yes", "accept defaults without prompting").option("--name <displayName>", "display name for your issuer profile").option("--badge <name>", 'name of the badge to send (default: "Quickstart Complete")').option("--description <text>", "badge description").option(
|
|
1117
|
+
"--profile-id <id>",
|
|
1118
|
+
"public handle for your profile (default: derived from the display name)"
|
|
1119
|
+
).option("--network <url>", "network tRPC URL (default: production)").option("--template", "send using a reusable template and hosted signing authority").option("--template-uri <uri>", "send from a specific template (implies --template)").option("--webhook-url <url>", "receive ISSUANCE_DELIVERED / ISSUANCE_CLAIMED at this URL").option("--suppress-delivery", "skip the claim email; you deliver inbox.claimUrl yourself").option(
|
|
1120
|
+
"--guardian-email <email>",
|
|
1121
|
+
"require a guardian's approval before the recipient can claim"
|
|
1122
|
+
).option("--json", "print a single JSON result on stdout").action(
|
|
1123
|
+
async (email, opts) => {
|
|
1124
|
+
out.json = !!opts.json;
|
|
1125
|
+
out.result = {};
|
|
1126
|
+
const didkit = fs__default$1["default"].readFile(
|
|
1127
|
+
require.resolve("@learncard/didkit-plugin/dist/didkit/didkit_wasm_bg.wasm")
|
|
1128
|
+
);
|
|
1129
|
+
try {
|
|
1130
|
+
await runSend(email, __spreadProps$4(__spreadValues$6({}, opts), { didkit }));
|
|
1131
|
+
if (out.json) {
|
|
1132
|
+
process.stdout.write(
|
|
1133
|
+
JSON.stringify(__spreadValues$6({ ok: true, command: "send" }, out.result)) + "\n"
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
process.exit(0);
|
|
1137
|
+
} catch (error) {
|
|
1138
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1139
|
+
const firstLine = message.split("\n")[0];
|
|
1140
|
+
if (out.json) {
|
|
1141
|
+
process.stdout.write(
|
|
1142
|
+
JSON.stringify({ ok: false, command: "send", error: firstLine }) + "\n"
|
|
1143
|
+
);
|
|
1144
|
+
process.exit(1);
|
|
1145
|
+
}
|
|
1146
|
+
console.error(`
|
|
1147
|
+
${firstLine}`);
|
|
1148
|
+
if (!/is not an email address/.test(firstLine))
|
|
1149
|
+
console.error(
|
|
1150
|
+
"Troubleshooting: https://docs.learncard.com/start-here/your-first-integration#if-something-goes-wrong"
|
|
1151
|
+
);
|
|
1152
|
+
process.exit(1);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
);
|
|
1156
|
+
const commandOptions = (command) => command.option("-y, --yes", "accept defaults without prompting").option("--profile-id <id>", "public handle for your issuer profile").option("--network <url>", "network tRPC URL or staging (default: production)").option("--json", "print a single JSON result on stdout");
|
|
1157
|
+
const runCommand = async (command, options, action, wrap = true) => {
|
|
1158
|
+
out.json = !!options.json;
|
|
1159
|
+
out.result = {};
|
|
1160
|
+
try {
|
|
1161
|
+
await action(
|
|
1162
|
+
fs__default$1["default"].readFile(require.resolve("@learncard/didkit-plugin/dist/didkit/didkit_wasm_bg.wasm"))
|
|
1163
|
+
);
|
|
1164
|
+
if (out.json && wrap) {
|
|
1165
|
+
process.stdout.write(JSON.stringify(__spreadValues$6({ ok: true, command }, out.result)) + "\n");
|
|
1166
|
+
}
|
|
1167
|
+
process.exit(process.exitCode || 0);
|
|
1168
|
+
} catch (error) {
|
|
1169
|
+
const message = error instanceof Error ? error.message.split("\n")[0] : "Command failed. Please try again.";
|
|
1170
|
+
if (out.json && wrap) {
|
|
1171
|
+
process.stdout.write(JSON.stringify({ ok: false, command, error: message }) + "\n");
|
|
1172
|
+
process.exit(1);
|
|
1173
|
+
}
|
|
1174
|
+
console.error(message);
|
|
1175
|
+
process.exit(1);
|
|
1176
|
+
}
|
|
1177
|
+
};
|
|
1178
|
+
commandOptions(
|
|
1179
|
+
commander.program.command("consent-contract").description("Connect a user's LearnCard to your platform.")
|
|
1180
|
+
).option("--name <name>", "contract name (default: issuer display name)").option(
|
|
1181
|
+
"--redirect-url <url>",
|
|
1182
|
+
"callback URL (default: http://localhost:3000/consent-callback)"
|
|
1183
|
+
).option("--description <text>", "what users see when asked to consent").option(
|
|
1184
|
+
"--needs-guardian-consent",
|
|
1185
|
+
"GameFlow: minors need a guardian to approve the connection"
|
|
1186
|
+
).action(
|
|
1187
|
+
(options) => runCommand("consent-contract", options, async (didkit) => {
|
|
1188
|
+
const { runConsentContract } = await Promise.resolve().then(function () { return consentContract; });
|
|
1189
|
+
await runConsentContract(__spreadProps$4(__spreadValues$6({}, options), { didkit }));
|
|
1190
|
+
})
|
|
1191
|
+
);
|
|
1192
|
+
commandOptions(commander.program.command("embed").description("Put a Claim button on your site.")).option("--name <name>", "integration name (default: issuer display name)").option(
|
|
1193
|
+
"--domains <origins>",
|
|
1194
|
+
"comma-separated origins (default: http://localhost:3000,http://localhost:5173)"
|
|
1195
|
+
).option("--rotate-key", "rotate the integration publishable key").action(
|
|
1196
|
+
(options) => runCommand("embed", options, async (didkit) => {
|
|
1197
|
+
const { runEmbed } = await Promise.resolve().then(function () { return embed; });
|
|
1198
|
+
await runEmbed(__spreadProps$4(__spreadValues$6({}, options), { didkit }));
|
|
1199
|
+
})
|
|
1200
|
+
);
|
|
1201
|
+
commandOptions(
|
|
1202
|
+
commander.program.command("setup-signing").description("Set up LearnCard to sign credentials for your project.")
|
|
1203
|
+
).option("--name <name>", "signing authority name (default: default-issuer)").option("--endpoint <url>", "register your own VC-API signing service instead (with --did)").option("--did <did>", "DID of your own signing service (with --endpoint)").action(
|
|
1204
|
+
(options) => runCommand("setup-signing", options, async (didkit) => {
|
|
1205
|
+
const { runSetupSigning } = await Promise.resolve().then(function () { return setupSigning$1; });
|
|
1206
|
+
await runSetupSigning(__spreadProps$4(__spreadValues$6({}, options), { didkit }));
|
|
1207
|
+
})
|
|
1208
|
+
);
|
|
1209
|
+
commandOptions(
|
|
1210
|
+
commander.program.command("token").description("Create a scoped API token and reusable send.sh.")
|
|
1211
|
+
).option("--name <name>", "auth grant name (default: cli-<date>)").option("--scope <scope>", "space-separated permissions (default: boosts:write)").option("--revoke <grantId>", "revoke an existing auth grant").option("--expires <days>", "token lifetime in days (default: no expiry)").option("--list", "list your auth grants").action(
|
|
1212
|
+
(options) => runCommand("token", options, async (didkit) => {
|
|
1213
|
+
const { runToken } = await Promise.resolve().then(function () { return token; });
|
|
1214
|
+
await runToken(__spreadProps$4(__spreadValues$6({}, options), { didkit }));
|
|
1215
|
+
})
|
|
1216
|
+
);
|
|
1217
|
+
commander.program.command("verify <file>").description("Verify a credential or presentation JSON file; use - for stdin.").option("--json", "print the raw verification result").action(
|
|
1218
|
+
(file, options) => runCommand(
|
|
1219
|
+
"verify",
|
|
1220
|
+
options,
|
|
1221
|
+
async (didkit) => {
|
|
1222
|
+
const { runVerify } = await Promise.resolve().then(function () { return verify; });
|
|
1223
|
+
await runVerify(file, __spreadProps$4(__spreadValues$6({}, options), { didkit }));
|
|
1224
|
+
},
|
|
1225
|
+
false
|
|
1226
|
+
)
|
|
1227
|
+
);
|
|
1228
|
+
commandOptions(
|
|
1229
|
+
commander.program.command("revoke <credentialUri>").description("Revoke or suspend an issued credential on the network.")
|
|
1230
|
+
).option("--suspend", "suspend instead of permanently revoking").option("--template-uri <uri>", "template URI if it cannot be read from the credential").option(
|
|
1231
|
+
"--recipient <profileId>",
|
|
1232
|
+
"recipient profile ID if it cannot be read from the credential"
|
|
1233
|
+
).action(
|
|
1234
|
+
(uri, options) => runCommand("revoke", options, async (didkit) => {
|
|
1235
|
+
const { runRevoke } = await Promise.resolve().then(function () { return revoke; });
|
|
1236
|
+
await runRevoke(uri, __spreadProps$4(__spreadValues$6({}, options), { didkit }));
|
|
1237
|
+
})
|
|
1238
|
+
);
|
|
1239
|
+
commandOptions(
|
|
1240
|
+
commander.program.command("status [activityId]").description("What happened to a credential you sent: created, delivered, claimed.")
|
|
1241
|
+
).option("--limit <n>", "how many recent sends to list (default: 20)").option(
|
|
1242
|
+
"--event <type>",
|
|
1243
|
+
"only list sends whose latest event is this: created|delivered|claimed|expired|failed"
|
|
1244
|
+
).action(
|
|
1245
|
+
(activityId, options) => runCommand("status", options, async (didkit) => {
|
|
1246
|
+
const { runStatus } = await Promise.resolve().then(function () { return status; });
|
|
1247
|
+
await runStatus(activityId, __spreadProps$4(__spreadValues$6({}, options), { didkit }));
|
|
1248
|
+
})
|
|
1249
|
+
);
|
|
1250
|
+
commander.program.command("open [target]").description(
|
|
1251
|
+
"Open the LearnCard app signed in as this project. Targets: portal (default), wallet, template, contract, integration."
|
|
1252
|
+
).option("-y, --yes", "accept defaults without prompting").option("--network <url>", "network tRPC URL or staging (default: production)").option("--app-url <url>", "LearnCard app URL for self-hosted or local networks").option("--url-fragment", "pass the seed in the URL fragment instead of the clipboard").option("--no-browser", "print the URL without opening a browser").option("--json", "print a single JSON result on stdout").action(
|
|
1253
|
+
(target, options) => runCommand("open", options, async () => {
|
|
1254
|
+
const { runOpen, OPEN_TARGETS } = await Promise.resolve().then(function () { return open; });
|
|
1255
|
+
if (target && !(target in OPEN_TARGETS))
|
|
1256
|
+
throw new Error(
|
|
1257
|
+
`Unknown target "${target}". Use one of: ${Object.keys(OPEN_TARGETS).join(", ")}.`
|
|
1258
|
+
);
|
|
1259
|
+
await runOpen(target, options);
|
|
1260
|
+
})
|
|
1261
|
+
);
|
|
1262
|
+
commandOptions(
|
|
1263
|
+
commander.program.command("webhook [email]").description("Know when your credential was claimed.")
|
|
1264
|
+
).option("--to <email>", "recipient email").option("--url <publicUrl>", "public HTTPS webhook URL").option("--port <n>", "receiver port (default: 8787)").option("--name <name>", "display name for your issuer profile").option(
|
|
1265
|
+
"--timeout <seconds>",
|
|
1266
|
+
"in --json mode, seconds to wait for webhook events (default: 60)"
|
|
1267
|
+
).option("--wait-for-claim", "in --json mode, also wait for ISSUANCE_CLAIMED before exiting").action(
|
|
1268
|
+
(email, options) => runCommand("webhook", options, async (didkit) => {
|
|
1269
|
+
const { runWebhook } = await Promise.resolve().then(function () { return webhook; });
|
|
1270
|
+
await runWebhook(email, __spreadProps$4(__spreadValues$6({}, options), { didkit }));
|
|
1271
|
+
})
|
|
1272
|
+
);
|
|
1273
|
+
const JOURNEY = [
|
|
1274
|
+
"send",
|
|
1275
|
+
"status",
|
|
1276
|
+
"setup-signing",
|
|
1277
|
+
"token",
|
|
1278
|
+
"webhook",
|
|
1279
|
+
"consent-contract",
|
|
1280
|
+
"embed",
|
|
1281
|
+
"verify",
|
|
1282
|
+
"revoke",
|
|
1283
|
+
"open",
|
|
1284
|
+
"init",
|
|
1285
|
+
"repl"
|
|
1286
|
+
];
|
|
1287
|
+
commander.program.name("learncard").description(
|
|
1288
|
+
"Issue, track, and verify credentials from the terminal. Every command shares the .env in the current folder."
|
|
1289
|
+
).showSuggestionAfterError().configureHelp({
|
|
1290
|
+
sortSubcommands: false,
|
|
1291
|
+
visibleCommands: (cmd) => [...cmd.commands].sort((a, b) => JOURNEY.indexOf(a.name()) - JOURNEY.indexOf(b.name()))
|
|
1292
|
+
}).addHelpText(
|
|
1293
|
+
"before",
|
|
1294
|
+
"\nStart here: npx @learncard/cli send you@example.com\nThen: npx @learncard/cli status\n"
|
|
1295
|
+
).addHelpText(
|
|
1296
|
+
"after",
|
|
1297
|
+
"\nEvery command: -y skips prompts, --json prints one machine-readable result, --network staging|<url> picks the network.\nEnvironment:\n LC_YES=1 Same as passing -y to every command.\n LCA_API_URL, NETWORK_URL Override service URLs for self-hosted networks.\n"
|
|
1298
|
+
);
|
|
1299
|
+
const runRepl = async (_seed = generateRandomSeed()) => {
|
|
377
1300
|
var _a, _b, _c, _d, _e;
|
|
378
1301
|
console.clear();
|
|
379
1302
|
const envSeed = (_a = process.env.LEARNCARD_CLI_SEED) != null ? _a : process.env.SEED;
|
|
380
1303
|
const seedInput = envSeed != null ? envSeed : _seed;
|
|
381
1304
|
const seed = seedInput.padStart(64, "0");
|
|
382
1305
|
console.log(
|
|
383
|
-
gradient__default["default"](["cyan", "green"])(figlet__default["default"].textSync("Learn Card", "Big Money-ne"))
|
|
1306
|
+
gradient__default["default"](["cyan", "green"])(figlet__default["default"].textSync("Learn Card", { font: "Big Money-ne" }))
|
|
384
1307
|
);
|
|
385
1308
|
console.log("Welcome to the Learn Card CLI!\n");
|
|
386
1309
|
console.log(`Your seed is ${seed}
|
|
@@ -391,13 +1314,13 @@ commander.program.version(packageJson.version).argument("[seed]").action(async (
|
|
|
391
1314
|
console.log("Creating wallet...");
|
|
392
1315
|
cliGlobals.seed = seed;
|
|
393
1316
|
cliGlobals.generateRandomSeed = generateRandomSeed;
|
|
394
|
-
cliGlobals.emptyLearnCard = init.emptyLearnCard;
|
|
395
|
-
cliGlobals.learnCardFromSeed = init.learnCardFromSeed;
|
|
396
|
-
cliGlobals.initLearnCard = init.initLearnCard;
|
|
397
|
-
const didkit = fs__default["default"].readFile(
|
|
1317
|
+
cliGlobals.emptyLearnCard = init$1.emptyLearnCard;
|
|
1318
|
+
cliGlobals.learnCardFromSeed = init$1.learnCardFromSeed;
|
|
1319
|
+
cliGlobals.initLearnCard = init$1.initLearnCard;
|
|
1320
|
+
const didkit = fs__default$1["default"].readFile(
|
|
398
1321
|
require.resolve("@learncard/didkit-plugin/dist/didkit/didkit_wasm_bg.wasm")
|
|
399
1322
|
);
|
|
400
|
-
const _learnCard = await init.initLearnCard({
|
|
1323
|
+
const _learnCard = await init$1.initLearnCard({
|
|
401
1324
|
seed,
|
|
402
1325
|
network: true,
|
|
403
1326
|
allowRemoteContexts: true,
|
|
@@ -424,14 +1347,11 @@ commander.program.version(packageJson.version).argument("[seed]").action(async (
|
|
|
424
1347
|
holderContinuity.exportLearnCardBundle,
|
|
425
1348
|
cliGlobals.learnCard
|
|
426
1349
|
);
|
|
427
|
-
cliGlobals.restoreLearnCardFromBundle = createRestoreLearnCardFromBundleHelper(
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
didkit
|
|
433
|
-
}
|
|
434
|
-
);
|
|
1350
|
+
cliGlobals.restoreLearnCardFromBundle = createRestoreLearnCardFromBundleHelper(holderContinuity.restoreLearnCardFromBundle, {
|
|
1351
|
+
network: true,
|
|
1352
|
+
allowRemoteContexts: true,
|
|
1353
|
+
didkit
|
|
1354
|
+
});
|
|
435
1355
|
cliGlobals.importLearnCardBundle = holderContinuity.importLearnCardBundle;
|
|
436
1356
|
cliGlobals.createLearnCardBundle = holderContinuity.createLearnCardBundle;
|
|
437
1357
|
cliGlobals.readLearnCardBundle = holderContinuity.readLearnCardBundle;
|
|
@@ -455,9 +1375,7 @@ commander.program.version(packageJson.version).argument("[seed]").action(async (
|
|
|
455
1375
|
console.log(`\u2502 ${g.restoreLearnCardFromBundle} \u2502 Restore original wallet from ZIP \u2502`);
|
|
456
1376
|
console.log("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
457
1377
|
console.log("");
|
|
458
|
-
console.log(
|
|
459
|
-
"For help/documentation regarding your wallet, please read the documentation at\n"
|
|
460
|
-
);
|
|
1378
|
+
console.log("For help/documentation regarding your wallet, please read the documentation at\n");
|
|
461
1379
|
console.log("https://docs.learncard.com/sdks/learncard-core/construction\n");
|
|
462
1380
|
console.log("To get a feel for what's possible, try some of the following commands\n");
|
|
463
1381
|
console.log(
|
|
@@ -501,4 +1419,963 @@ commander.program.version(packageJson.version).argument("[seed]").action(async (
|
|
|
501
1419
|
);
|
|
502
1420
|
console.log("");
|
|
503
1421
|
await startCliRepl(colorizeReplInput);
|
|
504
|
-
}
|
|
1422
|
+
};
|
|
1423
|
+
commandOptions(
|
|
1424
|
+
commander.program.command("init").description("Create this folder's issuer identity and profile without sending anything.").option("--name <displayName>", "display name for your issuer profile")
|
|
1425
|
+
).action(
|
|
1426
|
+
(options) => runCommand("init", options, async (didkit) => {
|
|
1427
|
+
const { runInit } = await Promise.resolve().then(function () { return init; });
|
|
1428
|
+
await runInit(__spreadProps$4(__spreadValues$6({}, options), { didkit }));
|
|
1429
|
+
})
|
|
1430
|
+
);
|
|
1431
|
+
commander.program.command("repl [seed]").description("Interactive JavaScript console with a LearnCard preloaded (advanced).").action(runRepl);
|
|
1432
|
+
commander.program.version(packageJson.version);
|
|
1433
|
+
if (process.argv.length <= 2 && process.stdin.isTTY) {
|
|
1434
|
+
runRepl();
|
|
1435
|
+
} else {
|
|
1436
|
+
commander.program.parse(process.argv);
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
const writeSnippet = async (filename, source) => {
|
|
1440
|
+
try {
|
|
1441
|
+
await fs__default["default"].writeFile(path__default["default"].join(process.cwd(), filename), source, { flag: "wx" });
|
|
1442
|
+
out.log(`Wrote ./${filename}`);
|
|
1443
|
+
return true;
|
|
1444
|
+
} catch (error) {
|
|
1445
|
+
if (error.code !== "EEXIST") throw error;
|
|
1446
|
+
out.log(`Kept existing ./${filename}`);
|
|
1447
|
+
return false;
|
|
1448
|
+
}
|
|
1449
|
+
};
|
|
1450
|
+
|
|
1451
|
+
var __defProp$5 = Object.defineProperty;
|
|
1452
|
+
var __defProps$3 = Object.defineProperties;
|
|
1453
|
+
var __getOwnPropDescs$3 = Object.getOwnPropertyDescriptors;
|
|
1454
|
+
var __getOwnPropSymbols$5 = Object.getOwnPropertySymbols;
|
|
1455
|
+
var __hasOwnProp$5 = Object.prototype.hasOwnProperty;
|
|
1456
|
+
var __propIsEnum$5 = Object.prototype.propertyIsEnumerable;
|
|
1457
|
+
var __defNormalProp$5 = (obj, key, value) => key in obj ? __defProp$5(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
1458
|
+
var __spreadValues$5 = (a, b) => {
|
|
1459
|
+
for (var prop in b || (b = {}))
|
|
1460
|
+
if (__hasOwnProp$5.call(b, prop))
|
|
1461
|
+
__defNormalProp$5(a, prop, b[prop]);
|
|
1462
|
+
if (__getOwnPropSymbols$5)
|
|
1463
|
+
for (var prop of __getOwnPropSymbols$5(b)) {
|
|
1464
|
+
if (__propIsEnum$5.call(b, prop))
|
|
1465
|
+
__defNormalProp$5(a, prop, b[prop]);
|
|
1466
|
+
}
|
|
1467
|
+
return a;
|
|
1468
|
+
};
|
|
1469
|
+
var __spreadProps$3 = (a, b) => __defProps$3(a, __getOwnPropDescs$3(b));
|
|
1470
|
+
const consentUrl = (uri, returnTo, network) => {
|
|
1471
|
+
const url = new URL("/consent-flow", appUrlFor(network));
|
|
1472
|
+
url.searchParams.set("uri", uri);
|
|
1473
|
+
url.searchParams.set("returnTo", returnTo);
|
|
1474
|
+
return url.toString();
|
|
1475
|
+
};
|
|
1476
|
+
const runConsentContract = async (options) => {
|
|
1477
|
+
var _a, _b;
|
|
1478
|
+
const project = await loadProject(process.cwd());
|
|
1479
|
+
const returnTo = options.redirectUrl || project.env.RETURN_TO || "http://localhost:3000/consent-callback";
|
|
1480
|
+
if (!["http:", "https:"].includes(new URL(returnTo).protocol)) {
|
|
1481
|
+
throw new Error("Redirect URL must use HTTP or HTTPS.");
|
|
1482
|
+
}
|
|
1483
|
+
const identity = await ensureIdentity(project, options);
|
|
1484
|
+
const learnCard = await connect(project, options);
|
|
1485
|
+
await ensureProfile(learnCard, identity, project);
|
|
1486
|
+
let uri = project.env[KEYS.CONTRACT_URI];
|
|
1487
|
+
if (!uri) {
|
|
1488
|
+
const prompts = createPrompts(options.yes);
|
|
1489
|
+
let name;
|
|
1490
|
+
try {
|
|
1491
|
+
name = (_a = options.name) != null ? _a : await prompts.ask("Contract name", identity.displayName);
|
|
1492
|
+
} finally {
|
|
1493
|
+
prompts.close();
|
|
1494
|
+
}
|
|
1495
|
+
const contract = __spreadProps$3(__spreadValues$5({
|
|
1496
|
+
name,
|
|
1497
|
+
description: (_b = options.description) != null ? _b : "Allow us to read your name and achievements and send achievements."
|
|
1498
|
+
}, options.needsGuardianConsent ? { needsGuardianConsent: true } : {}), {
|
|
1499
|
+
redirectUrl: returnTo,
|
|
1500
|
+
contract: {
|
|
1501
|
+
read: {
|
|
1502
|
+
personal: { name: { required: false } },
|
|
1503
|
+
credentials: { categories: { Achievement: { required: false } } }
|
|
1504
|
+
},
|
|
1505
|
+
write: {
|
|
1506
|
+
personal: {},
|
|
1507
|
+
credentials: { categories: { Achievement: { required: false } } }
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
});
|
|
1511
|
+
uri = await learnCard.invoke.createContract(contract);
|
|
1512
|
+
out.log("Created your consent contract.");
|
|
1513
|
+
} else {
|
|
1514
|
+
out.log("Reusing your saved consent contract.");
|
|
1515
|
+
}
|
|
1516
|
+
await saveProject(project, { [KEYS.CONTRACT_URI]: uri, RETURN_TO: returnTo });
|
|
1517
|
+
const services = resolveServices(project.env, options.network);
|
|
1518
|
+
const files = [];
|
|
1519
|
+
for (const [file, source] of Object.entries({
|
|
1520
|
+
"create-contract.mjs": CREATE_CONTRACT_MJS,
|
|
1521
|
+
"consent-callback.mjs": CONSENT_CALLBACK_MJS,
|
|
1522
|
+
"read-user-data.mjs": READ_USER_DATA_MJS,
|
|
1523
|
+
"issue-through-contract.mjs": ISSUE_THROUGH_CONTRACT_MJS
|
|
1524
|
+
})) {
|
|
1525
|
+
const localized = services.network === PRODUCTION_NETWORK ? source : localizeSnippet(
|
|
1526
|
+
source.replaceAll(
|
|
1527
|
+
"seed: SECURE_SEED, network: true",
|
|
1528
|
+
"seed: process.env.SECURE_SEED, network: true"
|
|
1529
|
+
),
|
|
1530
|
+
services
|
|
1531
|
+
);
|
|
1532
|
+
if (await writeSnippet(file, localized)) files.push(`./${file}`);
|
|
1533
|
+
}
|
|
1534
|
+
const contractConsentUrl = consentUrl(uri, returnTo, services.network);
|
|
1535
|
+
out.log(contractConsentUrl);
|
|
1536
|
+
if (![PRODUCTION_NETWORK, STAGING_NETWORK].includes(services.network)) {
|
|
1537
|
+
out.log(
|
|
1538
|
+
"Note: this is a production app URL; use an app connected to your local network to consent."
|
|
1539
|
+
);
|
|
1540
|
+
}
|
|
1541
|
+
out.log(
|
|
1542
|
+
"The tutorial create-contract.mjs creates a new contract and requires HTTPS; reuse CONTRACT_URI for this dev setup."
|
|
1543
|
+
);
|
|
1544
|
+
out.log("Read scripts require a verified CONSENT_VP, not a bare DID.");
|
|
1545
|
+
out.log("Next: node --env-file=.env consent-callback.mjs");
|
|
1546
|
+
out.log("See the contract in the app: npx @learncard/cli open contract");
|
|
1547
|
+
out.set({ contractUri: uri, consentUrl: contractConsentUrl, returnTo, files });
|
|
1548
|
+
};
|
|
1549
|
+
|
|
1550
|
+
var consentContract = /*#__PURE__*/Object.freeze({
|
|
1551
|
+
__proto__: null,
|
|
1552
|
+
consentUrl: consentUrl,
|
|
1553
|
+
runConsentContract: runConsentContract
|
|
1554
|
+
});
|
|
1555
|
+
|
|
1556
|
+
var __defProp$4 = Object.defineProperty;
|
|
1557
|
+
var __defProps$2 = Object.defineProperties;
|
|
1558
|
+
var __getOwnPropDescs$2 = Object.getOwnPropertyDescriptors;
|
|
1559
|
+
var __getOwnPropSymbols$4 = Object.getOwnPropertySymbols;
|
|
1560
|
+
var __hasOwnProp$4 = Object.prototype.hasOwnProperty;
|
|
1561
|
+
var __propIsEnum$4 = Object.prototype.propertyIsEnumerable;
|
|
1562
|
+
var __defNormalProp$4 = (obj, key, value) => key in obj ? __defProp$4(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
1563
|
+
var __spreadValues$4 = (a, b) => {
|
|
1564
|
+
for (var prop in b || (b = {}))
|
|
1565
|
+
if (__hasOwnProp$4.call(b, prop))
|
|
1566
|
+
__defNormalProp$4(a, prop, b[prop]);
|
|
1567
|
+
if (__getOwnPropSymbols$4)
|
|
1568
|
+
for (var prop of __getOwnPropSymbols$4(b)) {
|
|
1569
|
+
if (__propIsEnum$4.call(b, prop))
|
|
1570
|
+
__defNormalProp$4(a, prop, b[prop]);
|
|
1571
|
+
}
|
|
1572
|
+
return a;
|
|
1573
|
+
};
|
|
1574
|
+
var __spreadProps$2 = (a, b) => __defProps$2(a, __getOwnPropDescs$2(b));
|
|
1575
|
+
const validateDomains = (domains) => domains.split(",").map((domain) => {
|
|
1576
|
+
const result = types.LCNDomainOrOriginValidator.safeParse(domain.trim());
|
|
1577
|
+
if (!result.success)
|
|
1578
|
+
throw new Error(result.error.format()._errors.join("; ") || result.error.message);
|
|
1579
|
+
return result.data;
|
|
1580
|
+
});
|
|
1581
|
+
const scriptString = (value) => JSON.stringify(value).replace(/</g, "\\u003c");
|
|
1582
|
+
const personalizeClaimButton = (key, apiBaseUrl) => CLAIM_BUTTON_HTML.replace("'PUBLISHABLE_KEY_PLACEHOLDER'", () => scriptString(key)).replace(
|
|
1583
|
+
"'https://network.learncard.com/api'",
|
|
1584
|
+
() => scriptString(apiBaseUrl)
|
|
1585
|
+
);
|
|
1586
|
+
const runEmbed = async (options) => {
|
|
1587
|
+
var _a, _b;
|
|
1588
|
+
const domains = validateDomains(
|
|
1589
|
+
(_a = options.domains) != null ? _a : "http://localhost:3000,http://localhost:5173"
|
|
1590
|
+
);
|
|
1591
|
+
const project = await loadProject(process.cwd());
|
|
1592
|
+
const identity = await ensureIdentity(project, options);
|
|
1593
|
+
const learnCard = await connect(project, __spreadProps$2(__spreadValues$4({}, options), { lca: true }));
|
|
1594
|
+
await ensureProfile(learnCard, identity, project);
|
|
1595
|
+
await setupSigning(project, learnCard);
|
|
1596
|
+
let id = project.env[KEYS.INTEGRATION_ID];
|
|
1597
|
+
const reused = Boolean(id);
|
|
1598
|
+
if (!id) {
|
|
1599
|
+
id = await learnCard.invoke.addIntegration({
|
|
1600
|
+
name: (_b = options.name) != null ? _b : identity.displayName,
|
|
1601
|
+
whitelistedDomains: domains,
|
|
1602
|
+
guideType: "embed-claim"
|
|
1603
|
+
});
|
|
1604
|
+
await saveProject(project, { [KEYS.INTEGRATION_ID]: id });
|
|
1605
|
+
}
|
|
1606
|
+
let integration = await learnCard.invoke.getIntegration(id);
|
|
1607
|
+
if (!integration) throw new Error("Saved integration was not found. Check INTEGRATION_ID.");
|
|
1608
|
+
if (options.rotateKey) {
|
|
1609
|
+
if (!await learnCard.invoke.updateIntegration(id, { rotatePublishableKey: true })) {
|
|
1610
|
+
throw new Error("Could not rotate the publishable key.");
|
|
1611
|
+
}
|
|
1612
|
+
integration = await learnCard.invoke.getIntegration(id);
|
|
1613
|
+
if (!integration) throw new Error("Could not read the rotated integration.");
|
|
1614
|
+
}
|
|
1615
|
+
await saveProject(project, {
|
|
1616
|
+
[KEYS.INTEGRATION_ID]: id,
|
|
1617
|
+
[KEYS.PUBLISHABLE_KEY]: integration.publishableKey
|
|
1618
|
+
});
|
|
1619
|
+
const apiBase = resolveServices(project.env, options.network).network.replace(
|
|
1620
|
+
/\/trpc\/?$/,
|
|
1621
|
+
"/api"
|
|
1622
|
+
);
|
|
1623
|
+
const wroteClaimButton = await writeSnippet(
|
|
1624
|
+
"claim-button.html",
|
|
1625
|
+
personalizeClaimButton(integration.publishableKey, apiBase)
|
|
1626
|
+
);
|
|
1627
|
+
out.log(`Publishable key: ${integration.publishableKey}`);
|
|
1628
|
+
out.log(`Whitelisted origins: ${integration.whitelistedDomains.join(", ")}`);
|
|
1629
|
+
if (options.domains && reused) {
|
|
1630
|
+
out.log(
|
|
1631
|
+
"Existing integrations keep their saved origins; change them in the Developer Portal."
|
|
1632
|
+
);
|
|
1633
|
+
}
|
|
1634
|
+
if (options.rotateKey)
|
|
1635
|
+
out.log("Update the publishable key in any existing claim-button.html and deployed pages.");
|
|
1636
|
+
const firstDomain = integration.whitelistedDomains[0];
|
|
1637
|
+
const origin = (firstDomain == null ? void 0 : firstDomain.startsWith("http")) ? firstDomain : firstDomain ? `http://${firstDomain}` : void 0;
|
|
1638
|
+
out.log(
|
|
1639
|
+
origin ? `Next: Open claim-button.html from ${origin} (a whitelisted origin), not file://.
|
|
1640
|
+
See the integration in the app: npx @learncard/cli open integration` : "Next: add a whitelisted origin in the Developer Portal, then serve claim-button.html there, not file://."
|
|
1641
|
+
);
|
|
1642
|
+
out.set({
|
|
1643
|
+
integrationId: id,
|
|
1644
|
+
publishableKey: integration.publishableKey,
|
|
1645
|
+
whitelistedDomains: integration.whitelistedDomains,
|
|
1646
|
+
files: wroteClaimButton ? ["./claim-button.html"] : []
|
|
1647
|
+
});
|
|
1648
|
+
};
|
|
1649
|
+
|
|
1650
|
+
var embed = /*#__PURE__*/Object.freeze({
|
|
1651
|
+
__proto__: null,
|
|
1652
|
+
validateDomains: validateDomains,
|
|
1653
|
+
personalizeClaimButton: personalizeClaimButton,
|
|
1654
|
+
runEmbed: runEmbed
|
|
1655
|
+
});
|
|
1656
|
+
|
|
1657
|
+
var __defProp$3 = Object.defineProperty;
|
|
1658
|
+
var __defProps$1 = Object.defineProperties;
|
|
1659
|
+
var __getOwnPropDescs$1 = Object.getOwnPropertyDescriptors;
|
|
1660
|
+
var __getOwnPropSymbols$3 = Object.getOwnPropertySymbols;
|
|
1661
|
+
var __hasOwnProp$3 = Object.prototype.hasOwnProperty;
|
|
1662
|
+
var __propIsEnum$3 = Object.prototype.propertyIsEnumerable;
|
|
1663
|
+
var __defNormalProp$3 = (obj, key, value) => key in obj ? __defProp$3(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
1664
|
+
var __spreadValues$3 = (a, b) => {
|
|
1665
|
+
for (var prop in b || (b = {}))
|
|
1666
|
+
if (__hasOwnProp$3.call(b, prop))
|
|
1667
|
+
__defNormalProp$3(a, prop, b[prop]);
|
|
1668
|
+
if (__getOwnPropSymbols$3)
|
|
1669
|
+
for (var prop of __getOwnPropSymbols$3(b)) {
|
|
1670
|
+
if (__propIsEnum$3.call(b, prop))
|
|
1671
|
+
__defNormalProp$3(a, prop, b[prop]);
|
|
1672
|
+
}
|
|
1673
|
+
return a;
|
|
1674
|
+
};
|
|
1675
|
+
var __spreadProps$1 = (a, b) => __defProps$1(a, __getOwnPropDescs$1(b));
|
|
1676
|
+
const SCOPE_RESOURCES = [
|
|
1677
|
+
"boosts",
|
|
1678
|
+
"inbox",
|
|
1679
|
+
"credentials",
|
|
1680
|
+
"presentations",
|
|
1681
|
+
"profiles",
|
|
1682
|
+
"profileManagers",
|
|
1683
|
+
"connections",
|
|
1684
|
+
"contracts",
|
|
1685
|
+
"contracts-data",
|
|
1686
|
+
"signingAuthorities",
|
|
1687
|
+
"authGrants",
|
|
1688
|
+
"didMetadata",
|
|
1689
|
+
"claimHooks",
|
|
1690
|
+
"skills",
|
|
1691
|
+
"app-store",
|
|
1692
|
+
"integrations",
|
|
1693
|
+
"contact-methods",
|
|
1694
|
+
"activity",
|
|
1695
|
+
"storage"
|
|
1696
|
+
];
|
|
1697
|
+
const validateScope = (scope) => {
|
|
1698
|
+
for (const part of scope.trim().split(/\s+/).filter(Boolean)) {
|
|
1699
|
+
const [resource, action, extra] = part.split(":");
|
|
1700
|
+
if (extra !== void 0 || !resource || resource !== "*" && !SCOPE_RESOURCES.some((value) => value === resource)) {
|
|
1701
|
+
throw new Error(
|
|
1702
|
+
`Unknown scope resource in "${part}". Choose: ${SCOPE_RESOURCES.join(", ")}, or *.`
|
|
1703
|
+
);
|
|
1704
|
+
}
|
|
1705
|
+
if (!action || !["read", "write", "delete", "*"].includes(action)) {
|
|
1706
|
+
throw new Error(`Invalid action in "${part}". Use read, write, delete, or *.`);
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
return scope.trim().split(/\s+/).filter(Boolean).join(" ");
|
|
1710
|
+
};
|
|
1711
|
+
const runToken = async (options) => {
|
|
1712
|
+
var _a, _b, _c, _d, _e, _f;
|
|
1713
|
+
const scope = validateScope((_a = options.scope) != null ? _a : "boosts:write");
|
|
1714
|
+
const project = await loadProject(process.cwd());
|
|
1715
|
+
const identity = await ensureIdentity(project, __spreadProps$1(__spreadValues$3({}, options), { name: void 0 }));
|
|
1716
|
+
const learnCard = await connect(project, options);
|
|
1717
|
+
await ensureProfile(learnCard, identity, project);
|
|
1718
|
+
if (options.list) {
|
|
1719
|
+
const grants = (_b = await learnCard.invoke.getAuthGrants()) != null ? _b : [];
|
|
1720
|
+
for (const g of grants)
|
|
1721
|
+
out.log(
|
|
1722
|
+
`${g.id} ${((_c = g.status) != null ? _c : "").padEnd(8)} ${((_d = g.scope) != null ? _d : "").padEnd(22)} ${(_e = g.name) != null ? _e : ""}${g.expiresAt ? ` expires ${g.expiresAt.slice(0, 10)}` : ""}`
|
|
1723
|
+
);
|
|
1724
|
+
if (!grants.length) out.log("No auth grants yet.");
|
|
1725
|
+
out.set({
|
|
1726
|
+
grants: grants.map((g) => ({
|
|
1727
|
+
id: g.id,
|
|
1728
|
+
name: g.name,
|
|
1729
|
+
scope: g.scope,
|
|
1730
|
+
status: g.status,
|
|
1731
|
+
expiresAt: g.expiresAt
|
|
1732
|
+
}))
|
|
1733
|
+
});
|
|
1734
|
+
return;
|
|
1735
|
+
}
|
|
1736
|
+
if (options.revoke) {
|
|
1737
|
+
if (!await learnCard.invoke.revokeAuthGrant(options.revoke))
|
|
1738
|
+
throw new Error("Could not revoke auth grant.");
|
|
1739
|
+
out.log(`Revoked auth grant ${options.revoke}.`);
|
|
1740
|
+
out.log("Create a replacement: npx @learncard/cli token");
|
|
1741
|
+
out.set({ grantId: options.revoke });
|
|
1742
|
+
return;
|
|
1743
|
+
}
|
|
1744
|
+
const id = await learnCard.invoke.addAuthGrant(__spreadValues$3({
|
|
1745
|
+
name: (_f = options.name) != null ? _f : `cli-${( new Date()).toISOString().slice(0, 10)}`,
|
|
1746
|
+
scope
|
|
1747
|
+
}, options.expires ? {
|
|
1748
|
+
expiresAt: new Date(
|
|
1749
|
+
Date.now() + Number(options.expires) * 864e5
|
|
1750
|
+
).toISOString()
|
|
1751
|
+
} : {}));
|
|
1752
|
+
const token = await learnCard.invoke.getAPITokenForAuthGrant(id);
|
|
1753
|
+
await fs__default["default"].chmod(project.envPath, 384);
|
|
1754
|
+
await saveProject(project, { API_TOKEN: token, API_TOKEN_SCOPE: scope });
|
|
1755
|
+
out.log(`Created auth grant ${id}.`);
|
|
1756
|
+
out.log(
|
|
1757
|
+
"Warning: this token won't be shown again by this command. It is saved in .env; keep it private."
|
|
1758
|
+
);
|
|
1759
|
+
out.log(token);
|
|
1760
|
+
const file = path__default["default"].join(process.cwd(), "send.sh");
|
|
1761
|
+
let wroteSendSh = false;
|
|
1762
|
+
try {
|
|
1763
|
+
await fs__default["default"].writeFile(
|
|
1764
|
+
file,
|
|
1765
|
+
withEnvTokenLoader(
|
|
1766
|
+
localizeSnippet(SEND_SH, resolveServices(project.env, options.network))
|
|
1767
|
+
),
|
|
1768
|
+
{
|
|
1769
|
+
flag: "wx",
|
|
1770
|
+
mode: 448
|
|
1771
|
+
}
|
|
1772
|
+
);
|
|
1773
|
+
out.log("Wrote ./send.sh");
|
|
1774
|
+
wroteSendSh = true;
|
|
1775
|
+
} catch (error) {
|
|
1776
|
+
if (error.code !== "EEXIST") throw error;
|
|
1777
|
+
out.log("Kept existing ./send.sh");
|
|
1778
|
+
}
|
|
1779
|
+
out.log(
|
|
1780
|
+
"Next: save your send payload as request.json, then run sh ./send.sh (example: https://docs.learncard.com/start-here/your-first-integration)."
|
|
1781
|
+
);
|
|
1782
|
+
out.set({ grantId: id, scope, token, files: wroteSendSh ? ["./send.sh"] : [] });
|
|
1783
|
+
};
|
|
1784
|
+
|
|
1785
|
+
var token = /*#__PURE__*/Object.freeze({
|
|
1786
|
+
__proto__: null,
|
|
1787
|
+
SCOPE_RESOURCES: SCOPE_RESOURCES,
|
|
1788
|
+
validateScope: validateScope,
|
|
1789
|
+
runToken: runToken
|
|
1790
|
+
});
|
|
1791
|
+
|
|
1792
|
+
var __defProp$2 = Object.defineProperty;
|
|
1793
|
+
var __getOwnPropSymbols$2 = Object.getOwnPropertySymbols;
|
|
1794
|
+
var __hasOwnProp$2 = Object.prototype.hasOwnProperty;
|
|
1795
|
+
var __propIsEnum$2 = Object.prototype.propertyIsEnumerable;
|
|
1796
|
+
var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : /* @__PURE__ */ Symbol.for("Symbol." + name);
|
|
1797
|
+
var __defNormalProp$2 = (obj, key, value) => key in obj ? __defProp$2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
1798
|
+
var __spreadValues$2 = (a, b) => {
|
|
1799
|
+
for (var prop in b || (b = {}))
|
|
1800
|
+
if (__hasOwnProp$2.call(b, prop))
|
|
1801
|
+
__defNormalProp$2(a, prop, b[prop]);
|
|
1802
|
+
if (__getOwnPropSymbols$2)
|
|
1803
|
+
for (var prop of __getOwnPropSymbols$2(b)) {
|
|
1804
|
+
if (__propIsEnum$2.call(b, prop))
|
|
1805
|
+
__defNormalProp$2(a, prop, b[prop]);
|
|
1806
|
+
}
|
|
1807
|
+
return a;
|
|
1808
|
+
};
|
|
1809
|
+
var __forAwait = (obj, it, method) => (it = obj[__knownSymbol("asyncIterator")]) ? it.call(obj) : (obj = obj[__knownSymbol("iterator")](), it = {}, method = (key, fn) => (fn = obj[key]) && (it[key] = (arg) => new Promise((yes, no, done) => (arg = fn.call(obj, arg), done = arg.done, Promise.resolve(arg.value).then((value) => yes({ value, done }), no)))), method("next"), method("return"), it);
|
|
1810
|
+
const isPresentation = (value) => {
|
|
1811
|
+
if (!value || typeof value !== "object" || !("type" in value)) return false;
|
|
1812
|
+
const types = Array.isArray(value.type) ? value.type : [value.type];
|
|
1813
|
+
return types.includes("VerifiablePresentation");
|
|
1814
|
+
};
|
|
1815
|
+
const checkName = (value) => value === "signature" ? "proof" : value;
|
|
1816
|
+
const verificationFailed = (result) => Array.isArray(result) ? result.some((item) => item.status === "Failed") : result.errors.length > 0;
|
|
1817
|
+
const formatVerification = (result) => {
|
|
1818
|
+
if (Array.isArray(result))
|
|
1819
|
+
return result.map((item) => {
|
|
1820
|
+
const check = checkName(item.check);
|
|
1821
|
+
if (item.status === "Success") return `\u2713 ${check}`;
|
|
1822
|
+
return `${item.status === "Failed" ? "\u2717" : "!"} ${check}: ${item.details || item.message || "Check failed"}`;
|
|
1823
|
+
});
|
|
1824
|
+
return [
|
|
1825
|
+
...result.checks.map((check) => `\u2713 ${checkName(check)}`),
|
|
1826
|
+
...result.warnings.map((warning) => `! ${warning}`),
|
|
1827
|
+
...result.errors.map((error) => {
|
|
1828
|
+
const match = error.match(/^(.+?) error:\s*(.*)$/);
|
|
1829
|
+
return match ? `\u2717 ${checkName(match[1])}: ${match[2]}` : `\u2717 ${error}`;
|
|
1830
|
+
})
|
|
1831
|
+
];
|
|
1832
|
+
};
|
|
1833
|
+
const runVerify = async (file, options) => {
|
|
1834
|
+
let text;
|
|
1835
|
+
if (file === "-") {
|
|
1836
|
+
const chunks = [];
|
|
1837
|
+
try {
|
|
1838
|
+
for (var iter = __forAwait(process.stdin), more, temp, error; more = !(temp = await iter.next()).done; more = false) {
|
|
1839
|
+
const chunk = temp.value;
|
|
1840
|
+
chunks.push(Buffer.from(chunk));
|
|
1841
|
+
}
|
|
1842
|
+
} catch (temp) {
|
|
1843
|
+
error = [temp];
|
|
1844
|
+
} finally {
|
|
1845
|
+
try {
|
|
1846
|
+
more && (temp = iter.return) && await temp.call(iter);
|
|
1847
|
+
} finally {
|
|
1848
|
+
if (error)
|
|
1849
|
+
throw error[0];
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
text = Buffer.concat(chunks).toString("utf8");
|
|
1853
|
+
} else text = await fs__default["default"].readFile(file, "utf8");
|
|
1854
|
+
const input = JSON.parse(text);
|
|
1855
|
+
if (!input || typeof input !== "object" || !("type" in input))
|
|
1856
|
+
throw new Error("Expected a credential or presentation JSON object with a type.");
|
|
1857
|
+
const presentation = isPresentation(input);
|
|
1858
|
+
const types = Array.isArray(input.type) ? input.type : [input.type];
|
|
1859
|
+
if (!presentation && !types.includes("VerifiableCredential"))
|
|
1860
|
+
throw new Error("Expected VerifiableCredential or VerifiablePresentation in type.");
|
|
1861
|
+
const learnCard = await init$1.initLearnCard(__spreadValues$2({}, options.didkit && { didkit: options.didkit }));
|
|
1862
|
+
const result = presentation ? await learnCard.invoke.verifyPresentation(input) : options.json ? await learnCard.invoke.verifyCredential(input) : await learnCard.invoke.verifyCredential(input, {}, true);
|
|
1863
|
+
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
1864
|
+
else {
|
|
1865
|
+
for (const line of formatVerification(result)) console.log(line);
|
|
1866
|
+
console.log("Next: review warnings and issuer trust before accepting this credential.");
|
|
1867
|
+
}
|
|
1868
|
+
if (verificationFailed(result)) process.exitCode = 1;
|
|
1869
|
+
};
|
|
1870
|
+
|
|
1871
|
+
var verify = /*#__PURE__*/Object.freeze({
|
|
1872
|
+
__proto__: null,
|
|
1873
|
+
isPresentation: isPresentation,
|
|
1874
|
+
verificationFailed: verificationFailed,
|
|
1875
|
+
formatVerification: formatVerification,
|
|
1876
|
+
runVerify: runVerify
|
|
1877
|
+
});
|
|
1878
|
+
|
|
1879
|
+
const templateUriOf = (credential, options) => {
|
|
1880
|
+
const value = credential && typeof credential === "object" ? credential : {};
|
|
1881
|
+
const templateUri = options.templateUri || (typeof value.boostId === "string" ? value.boostId : void 0);
|
|
1882
|
+
if (!templateUri)
|
|
1883
|
+
throw new Error(
|
|
1884
|
+
"This credential was not issued from a template. Pass --template-uri <uri> and --recipient <profileId>."
|
|
1885
|
+
);
|
|
1886
|
+
return templateUri;
|
|
1887
|
+
};
|
|
1888
|
+
const recipientOf = (records, credentialUri) => {
|
|
1889
|
+
var _a;
|
|
1890
|
+
return (_a = records.find((record) => record.uri === credentialUri)) == null ? void 0 : _a.to.profileId;
|
|
1891
|
+
};
|
|
1892
|
+
const runRevoke = async (uri, options) => {
|
|
1893
|
+
var _a;
|
|
1894
|
+
const project = await loadProject(process.cwd());
|
|
1895
|
+
await ensureIdentity(project, options);
|
|
1896
|
+
const learnCard = await connect(project, options);
|
|
1897
|
+
const credential = options.templateUri && options.recipient ? void 0 : await learnCard.read.get(uri);
|
|
1898
|
+
const templateUri = templateUriOf(credential, options);
|
|
1899
|
+
let profileId = options.recipient;
|
|
1900
|
+
let cursor;
|
|
1901
|
+
while (!profileId) {
|
|
1902
|
+
const page = await learnCard.invoke.getPaginatedBoostRecipients(
|
|
1903
|
+
templateUri,
|
|
1904
|
+
100,
|
|
1905
|
+
cursor,
|
|
1906
|
+
true
|
|
1907
|
+
);
|
|
1908
|
+
profileId = recipientOf(page.records, uri);
|
|
1909
|
+
if (profileId || !page.hasMore) break;
|
|
1910
|
+
cursor = (_a = page.cursor) != null ? _a : void 0;
|
|
1911
|
+
}
|
|
1912
|
+
if (!profileId)
|
|
1913
|
+
throw new Error(
|
|
1914
|
+
`No recipient of ${templateUri} holds ${uri}. If it was sent to an email and not yet claimed, there is nothing to revoke; otherwise pass --recipient <profileId>.`
|
|
1915
|
+
);
|
|
1916
|
+
const target = { templateUri, profileId };
|
|
1917
|
+
const result = options.suspend ? await learnCard.invoke.suspendBoostRecipient(target.templateUri, target.profileId, uri) : await learnCard.invoke.revokeBoostRecipient(target.templateUri, target.profileId, uri);
|
|
1918
|
+
if (!result) throw new Error("The network did not update this credential.");
|
|
1919
|
+
const status = options.suspend ? "suspended" : "revoked";
|
|
1920
|
+
out.log(
|
|
1921
|
+
`${options.suspend ? "Suspended" : "Revoked"} ${uri}. Verifiers will see status: ${status} when they next refresh its status list; no propagation interval is documented.`
|
|
1922
|
+
);
|
|
1923
|
+
out.log("Only credentials with a credentialStatus entry support status-list verification.");
|
|
1924
|
+
out.log("Check the delivered JSON: npx @learncard/cli verify credential.json");
|
|
1925
|
+
out.set({ credentialUri: uri, templateUri, recipient: profileId, action: status });
|
|
1926
|
+
};
|
|
1927
|
+
|
|
1928
|
+
var revoke = /*#__PURE__*/Object.freeze({
|
|
1929
|
+
__proto__: null,
|
|
1930
|
+
templateUriOf: templateUriOf,
|
|
1931
|
+
recipientOf: recipientOf,
|
|
1932
|
+
runRevoke: runRevoke
|
|
1933
|
+
});
|
|
1934
|
+
|
|
1935
|
+
var __defProp$1 = Object.defineProperty;
|
|
1936
|
+
var __getOwnPropSymbols$1 = Object.getOwnPropertySymbols;
|
|
1937
|
+
var __hasOwnProp$1 = Object.prototype.hasOwnProperty;
|
|
1938
|
+
var __propIsEnum$1 = Object.prototype.propertyIsEnumerable;
|
|
1939
|
+
var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
1940
|
+
var __spreadValues$1 = (a, b) => {
|
|
1941
|
+
for (var prop in b || (b = {}))
|
|
1942
|
+
if (__hasOwnProp$1.call(b, prop))
|
|
1943
|
+
__defNormalProp$1(a, prop, b[prop]);
|
|
1944
|
+
if (__getOwnPropSymbols$1)
|
|
1945
|
+
for (var prop of __getOwnPropSymbols$1(b)) {
|
|
1946
|
+
if (__propIsEnum$1.call(b, prop))
|
|
1947
|
+
__defNormalProp$1(a, prop, b[prop]);
|
|
1948
|
+
}
|
|
1949
|
+
return a;
|
|
1950
|
+
};
|
|
1951
|
+
const LATEST_FIRST = (a, b) => b.timestamp.localeCompare(a.timestamp);
|
|
1952
|
+
const formatEvent = (event) => {
|
|
1953
|
+
var _a, _b;
|
|
1954
|
+
const when = event.timestamp.replace("T", " ").slice(0, 19);
|
|
1955
|
+
const failure = event.eventType === "FAILED" ? ` ${String((_b = (_a = event.metadata) == null ? void 0 : _a.error) != null ? _b : "")}` : "";
|
|
1956
|
+
return `${when} ${event.eventType.padEnd(9)} ${event.recipientIdentifier}${failure}`;
|
|
1957
|
+
};
|
|
1958
|
+
const summarize = (chain) => {
|
|
1959
|
+
var _a, _b, _c;
|
|
1960
|
+
const ordered = [...chain].sort(LATEST_FIRST);
|
|
1961
|
+
const latest = ordered[0];
|
|
1962
|
+
const claimed = chain.find((e) => e.eventType === "CLAIMED");
|
|
1963
|
+
return {
|
|
1964
|
+
state: (_a = latest == null ? void 0 : latest.eventType) != null ? _a : "UNKNOWN",
|
|
1965
|
+
recipient: latest == null ? void 0 : latest.recipientIdentifier,
|
|
1966
|
+
template: (_b = latest == null ? void 0 : latest.boost) == null ? void 0 : _b.name,
|
|
1967
|
+
credentialUri: (_c = claimed == null ? void 0 : claimed.credentialUri) != null ? _c : latest == null ? void 0 : latest.credentialUri,
|
|
1968
|
+
credentialStatus: latest == null ? void 0 : latest.status,
|
|
1969
|
+
events: ordered.map((e) => {
|
|
1970
|
+
var _a2;
|
|
1971
|
+
return __spreadValues$1({
|
|
1972
|
+
type: e.eventType,
|
|
1973
|
+
at: e.timestamp
|
|
1974
|
+
}, e.eventType === "FAILED" && ((_a2 = e.metadata) == null ? void 0 : _a2.error) ? { error: e.metadata.error } : {});
|
|
1975
|
+
})
|
|
1976
|
+
};
|
|
1977
|
+
};
|
|
1978
|
+
const runStatus = async (activityId, options) => {
|
|
1979
|
+
var _a, _b, _c;
|
|
1980
|
+
const project = await loadProject(process.cwd());
|
|
1981
|
+
if (!project.env.SECURE_SEED)
|
|
1982
|
+
throw new Error(
|
|
1983
|
+
"No SECURE_SEED in .env. Send something first: npx @learncard/cli send you@example.com"
|
|
1984
|
+
);
|
|
1985
|
+
await ensureIdentity(project, options);
|
|
1986
|
+
const learnCard = await connect(project, options);
|
|
1987
|
+
if (activityId) {
|
|
1988
|
+
const chain = await learnCard.invoke.getActivityChain({ activityId });
|
|
1989
|
+
if (!chain.length) throw new Error(`No activity found for ${activityId}.`);
|
|
1990
|
+
const summary = summarize(chain);
|
|
1991
|
+
out.log(`${summary.state} ${(_a = summary.template) != null ? _a : ""} \u2192 ${summary.recipient}`);
|
|
1992
|
+
for (const e of [...chain].sort(LATEST_FIRST)) out.log(` ${formatEvent(e)}`);
|
|
1993
|
+
if (summary.credentialUri) out.log(`Credential: ${summary.credentialUri}`);
|
|
1994
|
+
if (summary.state !== "CLAIMED")
|
|
1995
|
+
out.log("Not claimed yet. Re-run to check again, or use `webhook` to be told.");
|
|
1996
|
+
out.set(__spreadValues$1({ activityId }, summary));
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1999
|
+
const limit = Number((_b = options.limit) != null ? _b : 20);
|
|
2000
|
+
const eventType = (_c = options.event) == null ? void 0 : _c.toUpperCase();
|
|
2001
|
+
const page = await learnCard.invoke.getMyActivities(__spreadValues$1({
|
|
2002
|
+
limit
|
|
2003
|
+
}, eventType ? { eventType } : {}));
|
|
2004
|
+
const records = page.records;
|
|
2005
|
+
if (!records.length) {
|
|
2006
|
+
out.log("No sends yet. Try: npx @learncard/cli send you@example.com");
|
|
2007
|
+
out.set({ activities: [] });
|
|
2008
|
+
return;
|
|
2009
|
+
}
|
|
2010
|
+
out.log("activityId state recipient");
|
|
2011
|
+
for (const r of records)
|
|
2012
|
+
out.log(`${r.activityId} ${r.eventType.padEnd(9)} ${r.recipientIdentifier}`);
|
|
2013
|
+
if (page.hasMore)
|
|
2014
|
+
out.log(`\u2026more. Raise --limit or filter with --event claimed|delivered|created.`);
|
|
2015
|
+
out.log("Details for one: npx @learncard/cli status <activityId>");
|
|
2016
|
+
out.set({
|
|
2017
|
+
activities: records.map((r) => {
|
|
2018
|
+
var _a2;
|
|
2019
|
+
return {
|
|
2020
|
+
activityId: r.activityId,
|
|
2021
|
+
state: r.eventType,
|
|
2022
|
+
recipient: r.recipientIdentifier,
|
|
2023
|
+
template: (_a2 = r.boost) == null ? void 0 : _a2.name,
|
|
2024
|
+
at: r.timestamp
|
|
2025
|
+
};
|
|
2026
|
+
}),
|
|
2027
|
+
hasMore: page.hasMore
|
|
2028
|
+
});
|
|
2029
|
+
};
|
|
2030
|
+
|
|
2031
|
+
var status = /*#__PURE__*/Object.freeze({
|
|
2032
|
+
__proto__: null,
|
|
2033
|
+
formatEvent: formatEvent,
|
|
2034
|
+
summarize: summarize,
|
|
2035
|
+
runStatus: runStatus
|
|
2036
|
+
});
|
|
2037
|
+
|
|
2038
|
+
const OPEN_TARGETS = {
|
|
2039
|
+
portal: "the Developer Portal",
|
|
2040
|
+
wallet: "your wallet",
|
|
2041
|
+
template: "the template you last sent from",
|
|
2042
|
+
contract: "your consent contract",
|
|
2043
|
+
integration: "your Claim button's integration"
|
|
2044
|
+
};
|
|
2045
|
+
const openPath = (target, env) => {
|
|
2046
|
+
switch (target) {
|
|
2047
|
+
case "portal":
|
|
2048
|
+
return "/app-store/developer";
|
|
2049
|
+
case "wallet":
|
|
2050
|
+
return "/wallet";
|
|
2051
|
+
case "template":
|
|
2052
|
+
return env.TEMPLATE_URI ? `/app-store/developer?template=${encodeURIComponent(env.TEMPLATE_URI)}` : null;
|
|
2053
|
+
case "contract":
|
|
2054
|
+
return env.CONTRACT_URI ? `/app-store/developer?contract=${encodeURIComponent(env.CONTRACT_URI)}` : null;
|
|
2055
|
+
case "integration":
|
|
2056
|
+
return env.INTEGRATION_ID ? `/app-store/developer/integrations/${encodeURIComponent(env.INTEGRATION_ID)}` : null;
|
|
2057
|
+
}
|
|
2058
|
+
};
|
|
2059
|
+
const signInUrl = (appUrl, next, seedInFragment) => {
|
|
2060
|
+
const url = new URL("/developer/sign-in", appUrl);
|
|
2061
|
+
url.searchParams.set("next", next);
|
|
2062
|
+
if (seedInFragment) url.hash = `seed=${seedInFragment}`;
|
|
2063
|
+
return url.toString();
|
|
2064
|
+
};
|
|
2065
|
+
const CLIPBOARD_TTL_MS = 6e4;
|
|
2066
|
+
const isHttpUrl = (value) => {
|
|
2067
|
+
try {
|
|
2068
|
+
return ["http:", "https:"].includes(new URL(value).protocol);
|
|
2069
|
+
} catch (e) {
|
|
2070
|
+
return false;
|
|
2071
|
+
}
|
|
2072
|
+
};
|
|
2073
|
+
const launchBrowser = (url) => {
|
|
2074
|
+
const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? (
|
|
2075
|
+
// No shell involved: rundll32 receives the URL as a normal argv entry, so
|
|
2076
|
+
// cmd.exe metacharacters (&, |, ^, etc.) in the URL are never interpreted.
|
|
2077
|
+
["rundll32", ["url.dll,FileProtocolHandler", url]]
|
|
2078
|
+
) : ["xdg-open", [url]];
|
|
2079
|
+
node_child_process.spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
|
|
2080
|
+
};
|
|
2081
|
+
const waitForClipboardClear = (seed) => new Promise((resolve) => {
|
|
2082
|
+
let settled = false;
|
|
2083
|
+
const finish = async () => {
|
|
2084
|
+
if (settled) return;
|
|
2085
|
+
settled = true;
|
|
2086
|
+
clearTimeout(timer);
|
|
2087
|
+
process.removeListener("SIGINT", onSigint);
|
|
2088
|
+
if (await clipboard__default["default"].read().catch(() => "") === seed)
|
|
2089
|
+
await clipboard__default["default"].write("").catch(() => {
|
|
2090
|
+
});
|
|
2091
|
+
out.log("Clipboard cleared.");
|
|
2092
|
+
resolve();
|
|
2093
|
+
};
|
|
2094
|
+
const onSigint = () => {
|
|
2095
|
+
void finish();
|
|
2096
|
+
};
|
|
2097
|
+
const timer = setTimeout(() => void finish(), CLIPBOARD_TTL_MS);
|
|
2098
|
+
process.once("SIGINT", onSigint);
|
|
2099
|
+
});
|
|
2100
|
+
const impliesNoBrowser = (options, isTTY) => options.browser === false || !isTTY || !!options.json;
|
|
2101
|
+
const runOpen = async (target = "portal", options) => {
|
|
2102
|
+
const project = await loadProject(process.cwd());
|
|
2103
|
+
const seed = project.env.SECURE_SEED;
|
|
2104
|
+
if (!seed)
|
|
2105
|
+
throw new Error(
|
|
2106
|
+
"No SECURE_SEED in .env. Run a command that creates one first, e.g. npx @learncard/cli send you@example.com"
|
|
2107
|
+
);
|
|
2108
|
+
const path = openPath(target, project.env);
|
|
2109
|
+
if (!path)
|
|
2110
|
+
throw new Error(
|
|
2111
|
+
`Nothing saved for "${target}" yet. Run the command that creates it first (send --template, consent-contract, or embed).`
|
|
2112
|
+
);
|
|
2113
|
+
const { network } = resolveServices(project.env, options.network);
|
|
2114
|
+
if (options.appUrl && !isHttpUrl(options.appUrl))
|
|
2115
|
+
throw new Error("--app-url must be a valid http(s) URL.");
|
|
2116
|
+
if (!options.appUrl && network !== PRODUCTION_NETWORK && network !== STAGING_NETWORK)
|
|
2117
|
+
throw new Error(
|
|
2118
|
+
`${network} has no hosted LearnCard app. Pass --app-url <url> for the app connected to it.`
|
|
2119
|
+
);
|
|
2120
|
+
const appUrl = appUrlFor(network, options.appUrl);
|
|
2121
|
+
const noBrowser = impliesNoBrowser(options, !!process.stdout.isTTY);
|
|
2122
|
+
let url;
|
|
2123
|
+
let seedDelivery;
|
|
2124
|
+
let clipboardCleared;
|
|
2125
|
+
if (options.urlFragment) {
|
|
2126
|
+
url = signInUrl(appUrl, path, seed);
|
|
2127
|
+
out.log(
|
|
2128
|
+
"Warning: --url-fragment puts your seed in the browser URL (history, screenshots)."
|
|
2129
|
+
);
|
|
2130
|
+
seedDelivery = "fragment";
|
|
2131
|
+
} else if (out.json) {
|
|
2132
|
+
url = signInUrl(appUrl, path);
|
|
2133
|
+
out.log(
|
|
2134
|
+
"Seed not copied in --json mode. Paste it from .env, or re-run with --url-fragment."
|
|
2135
|
+
);
|
|
2136
|
+
seedDelivery = "none";
|
|
2137
|
+
} else {
|
|
2138
|
+
url = signInUrl(appUrl, path);
|
|
2139
|
+
try {
|
|
2140
|
+
await clipboard__default["default"].write(seed);
|
|
2141
|
+
out.log(`Copied your seed to the clipboard (clears in ${CLIPBOARD_TTL_MS / 1e3}s).`);
|
|
2142
|
+
seedDelivery = "clipboard";
|
|
2143
|
+
clipboardCleared = waitForClipboardClear(seed);
|
|
2144
|
+
} catch (e) {
|
|
2145
|
+
out.log(
|
|
2146
|
+
"Clipboard unavailable. Paste the seed from .env, or re-run with --url-fragment."
|
|
2147
|
+
);
|
|
2148
|
+
seedDelivery = "none";
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
if (!options.urlFragment) out.log('\u2192 Click "Paste from clipboard", then Sign in.');
|
|
2152
|
+
if (!noBrowser) launchBrowser(url);
|
|
2153
|
+
out.log(
|
|
2154
|
+
`Opening ${OPEN_TARGETS[target]}: ${options.urlFragment ? url.split("#")[0] + "#seed=\u2026" : url}`
|
|
2155
|
+
);
|
|
2156
|
+
out.set({ url, target, next: path, seedDelivery });
|
|
2157
|
+
await clipboardCleared;
|
|
2158
|
+
};
|
|
2159
|
+
|
|
2160
|
+
var open = /*#__PURE__*/Object.freeze({
|
|
2161
|
+
__proto__: null,
|
|
2162
|
+
OPEN_TARGETS: OPEN_TARGETS,
|
|
2163
|
+
openPath: openPath,
|
|
2164
|
+
signInUrl: signInUrl,
|
|
2165
|
+
impliesNoBrowser: impliesNoBrowser,
|
|
2166
|
+
runOpen: runOpen
|
|
2167
|
+
});
|
|
2168
|
+
|
|
2169
|
+
var __defProp = Object.defineProperty;
|
|
2170
|
+
var __defProps = Object.defineProperties;
|
|
2171
|
+
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
2172
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
2173
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
2174
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
2175
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
2176
|
+
var __spreadValues = (a, b) => {
|
|
2177
|
+
for (var prop in b || (b = {}))
|
|
2178
|
+
if (__hasOwnProp.call(b, prop))
|
|
2179
|
+
__defNormalProp(a, prop, b[prop]);
|
|
2180
|
+
if (__getOwnPropSymbols)
|
|
2181
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
2182
|
+
if (__propIsEnum.call(b, prop))
|
|
2183
|
+
__defNormalProp(a, prop, b[prop]);
|
|
2184
|
+
}
|
|
2185
|
+
return a;
|
|
2186
|
+
};
|
|
2187
|
+
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
2188
|
+
const webhookConfig = (env, options, runtime = process.env) => {
|
|
2189
|
+
var _a, _b, _c, _d;
|
|
2190
|
+
const port = Number((_c = (_b = (_a = options.port) != null ? _a : runtime.PORT) != null ? _b : env.PORT) != null ? _c : 8787);
|
|
2191
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
2192
|
+
throw new Error("Port must be 1\u201365535.");
|
|
2193
|
+
return { port, expectedDid: (_d = runtime.EXPECTED_NETWORK_DID) != null ? _d : env.EXPECTED_NETWORK_DID };
|
|
2194
|
+
};
|
|
2195
|
+
const closeWebhookReceiver = (server) => new Promise((resolve, reject) => {
|
|
2196
|
+
const timer = setTimeout(() => server.closeAllConnections(), 1e3);
|
|
2197
|
+
timer.unref();
|
|
2198
|
+
server.close((error) => {
|
|
2199
|
+
clearTimeout(timer);
|
|
2200
|
+
if (error) reject(error);
|
|
2201
|
+
else resolve();
|
|
2202
|
+
});
|
|
2203
|
+
});
|
|
2204
|
+
const loadWebhookModule = async () => {
|
|
2205
|
+
return import(`data:text/javascript;base64,${Buffer.from(WEBHOOK_MJS).toString("base64")}`);
|
|
2206
|
+
};
|
|
2207
|
+
const parseWebhookLogLine = (line) => {
|
|
2208
|
+
const [type, status, issuanceId, recipient] = line.trim().split(/\s+/);
|
|
2209
|
+
if (!type) return void 0;
|
|
2210
|
+
return __spreadValues(__spreadValues(__spreadValues({
|
|
2211
|
+
type
|
|
2212
|
+
}, status && { status }), issuanceId && { issuanceId }), recipient && { recipient });
|
|
2213
|
+
};
|
|
2214
|
+
const collectWebhookEvents = (timeoutMs, waitForClaim, registerStop) => new Promise((resolve) => {
|
|
2215
|
+
const events = [];
|
|
2216
|
+
const originalLog = console.log;
|
|
2217
|
+
const finish = () => {
|
|
2218
|
+
clearTimeout(timer);
|
|
2219
|
+
console.log = originalLog;
|
|
2220
|
+
resolve(events);
|
|
2221
|
+
};
|
|
2222
|
+
const timer = setTimeout(finish, Math.max(0, timeoutMs));
|
|
2223
|
+
registerStop == null ? void 0 : registerStop(finish);
|
|
2224
|
+
console.log = (...args) => {
|
|
2225
|
+
const [first] = args;
|
|
2226
|
+
const event = typeof first === "string" ? parseWebhookLogLine(first) : void 0;
|
|
2227
|
+
if (!event) return;
|
|
2228
|
+
events.push(event);
|
|
2229
|
+
const delivered = events.some((e) => e.type === "ISSUANCE_DELIVERED");
|
|
2230
|
+
const claimed = events.some((e) => e.type === "ISSUANCE_CLAIMED");
|
|
2231
|
+
if (delivered && (!waitForClaim || claimed)) finish();
|
|
2232
|
+
};
|
|
2233
|
+
});
|
|
2234
|
+
const runWebhook = async (email, options) => {
|
|
2235
|
+
var _a, _b, _c, _d;
|
|
2236
|
+
const project = await loadProject(process.cwd());
|
|
2237
|
+
const { port, expectedDid } = webhookConfig(project.env, options);
|
|
2238
|
+
const production = resolveServices(project.env, options.network).network === PRODUCTION_NETWORK;
|
|
2239
|
+
if (options.url && production && new URL(options.url).protocol !== "https:") {
|
|
2240
|
+
throw new Error("--url must be a public HTTPS URL when using the production network.");
|
|
2241
|
+
}
|
|
2242
|
+
const identity = await ensureIdentity(project, options);
|
|
2243
|
+
const learnCard = await connect(project, __spreadProps(__spreadValues({}, options), { lca: true }));
|
|
2244
|
+
await ensureProfile(learnCard, identity, project);
|
|
2245
|
+
const wroteWebhookMjs = await writeSnippet(
|
|
2246
|
+
"webhook.mjs",
|
|
2247
|
+
localizeSnippet(WEBHOOK_MJS, resolveServices(project.env, options.network))
|
|
2248
|
+
);
|
|
2249
|
+
const verifier = await init$1.initLearnCard(__spreadValues({}, options.didkit && { didkit: options.didkit }));
|
|
2250
|
+
const { createWebhookReceiver } = await loadWebhookModule();
|
|
2251
|
+
const server = createWebhookReceiver(verifier, expectedDid);
|
|
2252
|
+
server.requestTimeout = 5e3;
|
|
2253
|
+
await new Promise((resolve, reject) => {
|
|
2254
|
+
server.once("error", reject);
|
|
2255
|
+
server.listen(port, () => {
|
|
2256
|
+
server.removeListener("error", reject);
|
|
2257
|
+
resolve();
|
|
2258
|
+
});
|
|
2259
|
+
});
|
|
2260
|
+
out.log(`Listening on http://localhost:${port}`);
|
|
2261
|
+
try {
|
|
2262
|
+
if (!options.url) {
|
|
2263
|
+
out.log(
|
|
2264
|
+
`Next: expose port ${port} with ngrok/cloudflared and re-run with --url <publicUrl>. No credential sent.`
|
|
2265
|
+
);
|
|
2266
|
+
out.set({ port, files: wroteWebhookMjs ? ["./webhook.mjs"] : [] });
|
|
2267
|
+
return;
|
|
2268
|
+
}
|
|
2269
|
+
if (!expectedDid) {
|
|
2270
|
+
out.log(
|
|
2271
|
+
"Demo: signatures are verified, but any DID is accepted. Set EXPECTED_NETWORK_DID to your trusted network DID before production."
|
|
2272
|
+
);
|
|
2273
|
+
}
|
|
2274
|
+
const prompts = createPrompts(options.yes);
|
|
2275
|
+
let recipient;
|
|
2276
|
+
try {
|
|
2277
|
+
recipient = email || options.to || await prompts.ask("Recipient email (--to <email>)", "");
|
|
2278
|
+
} finally {
|
|
2279
|
+
prompts.close();
|
|
2280
|
+
}
|
|
2281
|
+
if (!recipient) throw new Error("Provide an email argument or --to <email>.");
|
|
2282
|
+
await setupSigning(project, learnCard, void 0, { persist: false });
|
|
2283
|
+
if (!project.env[KEYS.TEMPLATE_URI]) {
|
|
2284
|
+
const uri = await learnCard.invoke.createBoost(templateCredential(learnCard.id.did()), {
|
|
2285
|
+
name: DEFAULT_BADGE.name,
|
|
2286
|
+
category: "Achievement",
|
|
2287
|
+
status: "LIVE"
|
|
2288
|
+
});
|
|
2289
|
+
await saveProject(project, { [KEYS.TEMPLATE_URI]: uri });
|
|
2290
|
+
}
|
|
2291
|
+
const templateUri = project.env[KEYS.TEMPLATE_URI];
|
|
2292
|
+
const timeoutMs = (Number(options.timeout) > 0 ? Number(options.timeout) : 60) * 1e3;
|
|
2293
|
+
let stopCollectingEvents;
|
|
2294
|
+
const eventsPromise = out.json ? collectWebhookEvents(timeoutMs, !!options.waitForClaim, (stop) => {
|
|
2295
|
+
stopCollectingEvents = stop;
|
|
2296
|
+
}) : void 0;
|
|
2297
|
+
const onServerError = () => {
|
|
2298
|
+
};
|
|
2299
|
+
if (eventsPromise) server.on("error", onServerError);
|
|
2300
|
+
const result = await learnCard.invoke.send({
|
|
2301
|
+
type: "boost",
|
|
2302
|
+
recipient,
|
|
2303
|
+
templateUri,
|
|
2304
|
+
options: { webhookUrl: options.url }
|
|
2305
|
+
}).catch((error) => {
|
|
2306
|
+
if (eventsPromise) {
|
|
2307
|
+
server.removeListener("error", onServerError);
|
|
2308
|
+
stopCollectingEvents == null ? void 0 : stopCollectingEvents();
|
|
2309
|
+
}
|
|
2310
|
+
throw error;
|
|
2311
|
+
});
|
|
2312
|
+
out.log(`Sent. Watch for ISSUANCE_DELIVERED ${(_b = (_a = result.inbox) == null ? void 0 : _a.status) != null ? _b : ""}.`);
|
|
2313
|
+
out.log(
|
|
2314
|
+
((_c = result.inbox) == null ? void 0 : _c.status) === "PENDING" ? `Next: click the claim link in your email to see ISSUANCE_CLAIMED. Ctrl+C to stop.` : "Next: this recipient already has LearnCard; no claim event is expected. Ctrl+C to stop."
|
|
2315
|
+
);
|
|
2316
|
+
if (eventsPromise) {
|
|
2317
|
+
const events = await eventsPromise;
|
|
2318
|
+
server.removeListener("error", onServerError);
|
|
2319
|
+
out.set({
|
|
2320
|
+
port,
|
|
2321
|
+
url: options.url,
|
|
2322
|
+
templateUri,
|
|
2323
|
+
issuanceId: (_d = result.inbox) == null ? void 0 : _d.issuanceId,
|
|
2324
|
+
events
|
|
2325
|
+
});
|
|
2326
|
+
return;
|
|
2327
|
+
}
|
|
2328
|
+
await new Promise((resolve, reject) => {
|
|
2329
|
+
const stop = () => {
|
|
2330
|
+
cleanup();
|
|
2331
|
+
resolve();
|
|
2332
|
+
};
|
|
2333
|
+
const fail = (error) => {
|
|
2334
|
+
cleanup();
|
|
2335
|
+
reject(error);
|
|
2336
|
+
};
|
|
2337
|
+
const cleanup = () => {
|
|
2338
|
+
process.removeListener("SIGINT", stop);
|
|
2339
|
+
process.removeListener("SIGTERM", stop);
|
|
2340
|
+
server.removeListener("error", fail);
|
|
2341
|
+
};
|
|
2342
|
+
process.once("SIGINT", stop);
|
|
2343
|
+
process.once("SIGTERM", stop);
|
|
2344
|
+
server.once("error", fail);
|
|
2345
|
+
});
|
|
2346
|
+
} finally {
|
|
2347
|
+
await closeWebhookReceiver(server);
|
|
2348
|
+
}
|
|
2349
|
+
};
|
|
2350
|
+
|
|
2351
|
+
var webhook = /*#__PURE__*/Object.freeze({
|
|
2352
|
+
__proto__: null,
|
|
2353
|
+
webhookConfig: webhookConfig,
|
|
2354
|
+
closeWebhookReceiver: closeWebhookReceiver,
|
|
2355
|
+
loadWebhookModule: loadWebhookModule,
|
|
2356
|
+
parseWebhookLogLine: parseWebhookLogLine,
|
|
2357
|
+
collectWebhookEvents: collectWebhookEvents,
|
|
2358
|
+
runWebhook: runWebhook
|
|
2359
|
+
});
|
|
2360
|
+
|
|
2361
|
+
const runInit = async (options) => {
|
|
2362
|
+
const project = await loadProject(process.cwd());
|
|
2363
|
+
const fresh = !project.env.SECURE_SEED;
|
|
2364
|
+
const identity = await ensureIdentity(project, options);
|
|
2365
|
+
const learnCard = await connect(project, options);
|
|
2366
|
+
await ensureProfile(learnCard, identity, project);
|
|
2367
|
+
out.set({
|
|
2368
|
+
profileId: identity.profileId,
|
|
2369
|
+
displayName: identity.displayName,
|
|
2370
|
+
did: learnCard.id.did(),
|
|
2371
|
+
created: fresh,
|
|
2372
|
+
envPath: project.envPath
|
|
2373
|
+
});
|
|
2374
|
+
out.log(fresh ? "Ready. Your identity is in .env (keep it out of git)." : "Already set up.");
|
|
2375
|
+
out.log("Next: npx @learncard/cli send you@example.com");
|
|
2376
|
+
};
|
|
2377
|
+
|
|
2378
|
+
var init = /*#__PURE__*/Object.freeze({
|
|
2379
|
+
__proto__: null,
|
|
2380
|
+
runInit: runInit
|
|
2381
|
+
});
|