@learnpack/learnpack 5.0.357 → 5.0.359

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.
@@ -31,13 +31,15 @@ const misc_1 = require("../utils/misc");
31
31
  const configBuilder_1 = require("../utils/configBuilder");
32
32
  const creatorUtilities_2 = require("../utils/creatorUtilities");
33
33
  const sidebarGenerator_1 = require("../utils/sidebarGenerator");
34
- const publish_1 = require("./publish");
34
+ const assetResolver_1 = require("../utils/assetResolver");
35
+ const assetSync_1 = require("../utils/assetSync");
35
36
  const export_1 = require("../utils/export");
36
37
  const router_1 = require("../services/ingest/http/router");
37
38
  const bucketIo_1 = require("../utils/coursePackage/bucketIo");
38
39
  const learnJson_1 = require("../utils/coursePackage/learnJson");
39
40
  const sidebar_1 = require("../utils/coursePackage/sidebar");
40
41
  const packageManifest_1 = require("../utils/packageManifest");
42
+ const branding_1 = require("../utils/branding");
41
43
  const gcsBucketName_1 = require("../utils/gcsBucketName");
42
44
  const syllabusSync_1 = require("../utils/syllabusSync");
43
45
  const publishJournal_1 = require("../utils/publishJournal");
@@ -318,94 +320,34 @@ const cleanFormState = (formState) => {
318
320
  const cleanFormStateForSyllabus = (formState) => {
319
321
  return Object.assign(Object.assign({}, formState), { description: formState.description, technologies: formState.technologies, contentIndex: formState.contentIndex, purposse: undefined, duration: undefined, hasContentIndex: undefined, variables: undefined, currentStep: undefined, language: undefined, isCompleted: undefined });
320
322
  };
321
- const getLocalizedValue = (translations, lang, fallbackLangs = ["en", "us"]) => {
322
- if (!translations || typeof translations !== "object")
323
- return "";
324
- const direct = translations[lang];
325
- if (typeof direct === "string" && direct.trim().length > 0)
326
- return direct;
327
- for (const fb of fallbackLangs) {
328
- const v = translations[fb];
329
- if (typeof v === "string" && v.trim().length > 0)
330
- return v;
331
- }
332
- const firstKey = Object.keys(translations)[0];
333
- const first = firstKey ? translations[firstKey] : "";
334
- return typeof first === "string" ? first : "";
335
- };
336
- function assetSyncErrorDetail(err) {
337
- if (typeof (err === null || err === void 0 ? void 0 : err.detail) === "string")
338
- return err.detail;
339
- if (typeof (err === null || err === void 0 ? void 0 : err.message) === "string")
340
- return err.message;
341
- try {
342
- return JSON.stringify(err);
343
- }
344
- catch (_a) {
345
- return String(err);
346
- }
347
- }
348
323
  const createMultiLangAsset = async (bucket, rigoToken, bcToken, courseSlug, courseJson, deployUrl, academyId) => {
349
- var _a;
350
- const learnpackId = await (0, api_1.resolveLearnpackPackageId)(rigoToken, courseSlug);
351
- if (learnpackId === null) {
352
- return {
353
- errors: [
354
- {
355
- kind: "package_error",
356
- error: {
357
- detail: "Could not resolve Learnpack package id; assets not synced to Breathecode.",
358
- },
359
- },
360
- ],
361
- };
362
- }
363
- const availableLangs = Object.keys(courseJson.title);
364
- console.log("AVAILABLE LANGUAGES to upload asset", availableLangs);
365
- const all_translations = [];
366
- const errors = [];
367
- for (const lang of availableLangs) {
368
- // eslint-disable-next-line no-await-in-loop
369
- const indexReadme = await bucket.file(`courses/${courseSlug}/README${(0, creatorUtilities_1.getReadmeExtension)(lang)}`);
370
- let indexReadmeString = "";
324
+ const readReadme = async (lang) => {
325
+ const indexReadme = bucket.file(`courses/${courseSlug}/README${(0, creatorUtilities_1.getReadmeExtension)(lang)}`);
371
326
  try {
372
- // eslint-disable-next-line no-await-in-loop
373
- const [indexReadmeContent] = await indexReadme.download();
374
- indexReadmeString = indexReadmeContent.toString();
327
+ const [content] = await indexReadme.download();
328
+ return content.toString();
375
329
  }
376
330
  catch (error) {
377
331
  console.error("Error downloading index readme", error);
378
332
  // TODO: Trigger generation of the index readme
379
- indexReadmeString = "";
380
- }
381
- const b64IndexReadme = buffer_1.Buffer.from(indexReadmeString).toString("base64");
382
- try {
383
- // eslint-disable-next-line no-await-in-loop
384
- const asset = await (0, publish_1.handleAssetCreation)({ token: bcToken, rigobotToken: rigoToken.trim() }, courseJson, lang, deployUrl, b64IndexReadme, academyId, learnpackId, undefined, all_translations);
385
- if (!asset) {
386
- errors.push({
387
- kind: "lang_error",
388
- lang,
389
- error: { detail: "Failed to create asset" },
390
- });
391
- console.log("No se pudo crear el asset, saltando idioma", lang);
392
- continue;
393
- }
394
- all_translations.push(asset.slug);
395
- }
396
- catch (error) {
397
- const errorData = error && typeof error === "object" && "response" in error ?
398
- ((_a = error.response) === null || _a === void 0 ? void 0 : _a.data) || error :
399
- error;
400
- errors.push({
401
- kind: "lang_error",
402
- lang,
403
- error: { detail: assetSyncErrorDetail(errorData) },
404
- });
405
- console.error(`Error creating asset for language ${lang}:`, error);
333
+ return "";
406
334
  }
335
+ };
336
+ const result = await (0, assetSync_1.syncPackageAssets)({
337
+ bcToken,
338
+ rigoToken,
339
+ packageSlug: courseSlug,
340
+ learnJson: courseJson,
341
+ deployUrl,
342
+ academyId,
343
+ readReadme,
344
+ });
345
+ for (const warning of result.warnings)
346
+ console.warn("[assets]", warning);
347
+ for (const synced of result.synced) {
348
+ console.log(`[assets] ${synced.action} "${synced.lang}": #${synced.asset.id} ${synced.asset.slug}`);
407
349
  }
408
- return { errors };
350
+ return { errors: result.errors };
409
351
  };
410
352
  const lessonCleaner = (lesson) => {
411
353
  return Object.assign(Object.assign({}, lesson), { duration: undefined, generated: undefined, status: undefined, translations: undefined, uid: undefined, initialContent: undefined, locked: undefined });
@@ -733,21 +675,88 @@ async function continueWithNextLesson(courseSlug, currentExerciseIndex, rigoToke
733
675
  }) });
734
676
  await saveSyllabus(courseSlug, newSyllabus, bucket);
735
677
  }
736
- const fixPreviewUrl = (slug, previewUrl) => {
737
- if (!previewUrl) {
738
- return null;
678
+ /**
679
+ * The `og:image` of a published course.
680
+ *
681
+ * A published package is served from `{slug}.learn-pack.com`, so the course's
682
+ * own `preview.png` is the right card whenever it exists. This used to go
683
+ * through a helper that returned null for an empty preview, and both call sites
684
+ * passed an empty string — so every published course, preview or not, fell back
685
+ * to a shared SVG that no unfurl crawler can render.
686
+ */
687
+ const resolveCoursePreviewUrl = async (bucket, slug, configuredPreview) => {
688
+ // `config` is parsed from a config.json in the bucket, so its declared types
689
+ // are an assertion rather than a guarantee: an old or hand-edited course can
690
+ // carry anything here, and a throw would fail the whole publication.
691
+ if (typeof configuredPreview === "string" &&
692
+ configuredPreview.startsWith("http")) {
693
+ return configuredPreview;
694
+ }
695
+ try {
696
+ const [exists] = await bucket.file(`courses/${slug}/preview.png`).exists();
697
+ if (exists) {
698
+ return (0, packageManifest_1.resolvePreviewUrl)(slug);
699
+ }
739
700
  }
740
- if (previewUrl.startsWith("http")) {
741
- return previewUrl;
701
+ catch (error) {
702
+ console.error(`WARNING: could not check preview.png for ${slug}, using the brand card`, error);
742
703
  }
743
- const expectedUrl = `https://${slug}.learn-pack.com/preview.png`;
744
- return expectedUrl;
704
+ return (0, branding_1.getFallbackPreviewUrl)();
745
705
  };
746
706
  const getTitleFromHTML = (html) => {
747
707
  const titleRegex = /<title>(.*?)<\/title>/;
748
708
  const titleMatch = html.match(titleRegex);
749
709
  return titleMatch ? titleMatch[1] : null;
750
710
  };
711
+ const escapeHtmlAttribute = (value) => value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
712
+ /**
713
+ * The IDE shell served to draft courses, with its meta tags filled in.
714
+ *
715
+ * The shell ships with `{{title}}`, `{{description}}` and `{{preview}}`
716
+ * placeholders that only get substituted when a course is published. Drafts were
717
+ * served the raw template, so a link shared in Slack or WhatsApp unfurled to a
718
+ * literal `{{title}}` / `{{description}}`.
719
+ */
720
+ let draftShellTemplate = null;
721
+ const loadDraftShellTemplate = () => {
722
+ const file = path.resolve(__dirname, "../ui/_app/index.html");
723
+ draftShellTemplate = fs
724
+ .readFileSync(file, "utf-8")
725
+ // A draft is not a real Course, so it must not emit Course structured data.
726
+ // The match is anchored to the typed opening tag and non-greedy on purpose:
727
+ // the shell also carries an inline <script> that loads the app bundle from
728
+ // CloudFront, and a looser pattern would swallow it and blank the page.
729
+ .replace(/[^\S\n]*<script type="application\/ld\+json">[\S\s]*?<\/script>\n?/, "");
730
+ return draftShellTemplate;
731
+ };
732
+ const renderDraftShell = (req) => {
733
+ const template = draftShellTemplate || loadDraftShellTemplate();
734
+ // Only the path: a preview URL can carry tokens in its query string, and
735
+ // og:url ends up in whatever crawler cache unfurls the link.
736
+ const canonicalUrl = escapeHtmlAttribute(`${(0, branding_1.getPublicHost)()}${req.path}`);
737
+ return (template
738
+ .replace(/<title>.*<\/title>/, `<title>${branding_1.DRAFT_PREVIEW_TITLE}</title>`)
739
+ .replace(/{{title}}/g, branding_1.DRAFT_PREVIEW_TITLE)
740
+ .replace(/{{description}}/g, branding_1.DRAFT_PREVIEW_DESCRIPTION)
741
+ .replace(/{{preview}}/g, (0, branding_1.getFallbackPreviewUrl)())
742
+ .replace("</head>", ` <meta property="og:url" content="${canonicalUrl}" />\n` +
743
+ ` <meta name="robots" content="noindex, nofollow, noarchive" />\n </head>`)
744
+ // Safety net for placeholders a future shell may add: shipping an empty
745
+ // value to a crawler beats shipping "{{whatever}}".
746
+ .replace(/{{\w+}}/g, ""));
747
+ };
748
+ /**
749
+ * Drafts are unlisted work in progress. `noindex` goes out as both a meta tag
750
+ * and a header so it reaches crawlers that skip the markup; a `robots.txt`
751
+ * `Disallow` would be the wrong tool, since it blocks the fetch itself and
752
+ * would stop Slack and X from unfurling the link at all.
753
+ */
754
+ const sendDraftShell = (req, res) => {
755
+ res
756
+ .type("html")
757
+ .set("X-Robots-Tag", "noindex, nofollow")
758
+ .send(renderDraftShell(req));
759
+ };
751
760
  /** How often the notification is touched while a translation is running. */
752
761
  const SYNC_HEARTBEAT_INTERVAL_MS = 60 * 1000;
753
762
  /**
@@ -1045,6 +1054,10 @@ class ServeCommand extends SessionCommand_1.default {
1045
1054
  await (0, file_1.decompress)(`${__dirname}/../ui/app.tar.gz`, `${__dirname}/../ui/_app/`);
1046
1055
  const localAppPath = path.resolve(__dirname, "../ui/_app");
1047
1056
  // app.use(express.static(localAppPath))
1057
+ // Warm the draft shell now that _app is on disk. Doing it here rather than
1058
+ // on first request means a corrupt tarball fails at boot, with a stack
1059
+ // trace, instead of on some user's preview link.
1060
+ loadDraftShellTemplate();
1048
1061
  app.post("/upload", async (req, res) => {
1049
1062
  const { content, destination } = req.body;
1050
1063
  // console.log("UPLOAD", content, destination)
@@ -2160,8 +2173,7 @@ class ServeCommand extends SessionCommand_1.default {
2160
2173
  console.log("GET /");
2161
2174
  const slug = req.query.slug;
2162
2175
  if (slug) {
2163
- const file = path.resolve(__dirname, "../ui/_app/index.html");
2164
- res.sendFile(file);
2176
+ sendDraftShell(req, res);
2165
2177
  }
2166
2178
  else {
2167
2179
  res.redirect("https://learnpack.co");
@@ -2169,12 +2181,16 @@ class ServeCommand extends SessionCommand_1.default {
2169
2181
  });
2170
2182
  app.get("/preview/:slug", async (req, res) => {
2171
2183
  console.log("GET COURSE PREVIEW /");
2172
- const file = path.resolve(__dirname, "../ui/_app/index.html");
2173
- res.sendFile(file);
2184
+ sendDraftShell(req, res);
2174
2185
  });
2175
2186
  app.get("/preview/:slug/webview", async (req, res) => {
2176
- const file = path.resolve(__dirname, "../ui/_app/index.html");
2177
- res.sendFile(file);
2187
+ sendDraftShell(req, res);
2188
+ });
2189
+ // The brand card used as og:image by drafts, and as the last-resort
2190
+ // fallback by published courses without a preview of their own.
2191
+ app.get("/og-image.png", (req, res) => {
2192
+ res.set("Cache-Control", "public, max-age=86400");
2193
+ res.sendFile(path.resolve(__dirname, "../ui/static/og-image.png"));
2178
2194
  });
2179
2195
  app.get("/config", async (req, res) => {
2180
2196
  const courseSlug = req.query.slug;
@@ -3638,52 +3654,47 @@ class ServeCommand extends SessionCommand_1.default {
3638
3654
  error: "Authentication failed, missing breathecode token",
3639
3655
  });
3640
3656
  }
3657
+ // Older IDEs only send the Breathecode token; without the Rigobot
3658
+ // token the lookup falls back to the title-derived slugs.
3659
+ const rigoToken = req.header("x-rigo-token");
3641
3660
  const configFile = await bucket.file(`courses/${slug}/.learn/config.json`);
3642
3661
  const [configContent] = await configFile.download();
3643
3662
  const configJson = JSON.parse(configContent.toString());
3644
3663
  const { config } = configJson;
3645
- const availableLangs = Object.keys(config.title || {});
3646
- let isPublished = false;
3647
- // Collect academy ids from all existing assets
3648
- const foundAcademyIds = [];
3649
- const slugsToCheck = availableLangs
3650
- .map(lang => {
3651
- const assetTitle = getLocalizedValue(config.title, lang);
3652
- if (!assetTitle)
3653
- return null;
3654
- const assetSlug = (0, creatorUtilities_2.slugify)(assetTitle).slice(0, 47);
3655
- return `${assetSlug}-${lang}`;
3656
- })
3657
- .filter((slug) => slug !== null);
3658
- const assetChecks = await Promise.all(slugsToCheck.map(slug => (0, api_1.doesAssetExists)(bcToken, slug)));
3659
- for (const { exists, academyId: existingAcademyId } of assetChecks) {
3660
- if (exists) {
3661
- isPublished = true;
3662
- if (existingAcademyId !== undefined) {
3663
- foundAcademyIds.push(existingAcademyId);
3664
- }
3664
+ const resolution = await (0, assetResolver_1.resolvePackageAssets)({
3665
+ bcToken,
3666
+ rigoToken,
3667
+ packageSlug: slug,
3668
+ learnJson: config,
3669
+ });
3670
+ for (const warning of resolution.warnings) {
3671
+ console.warn("[package-academy]", warning);
3672
+ }
3673
+ const isPublished = (0, assetResolver_1.hasPublishedAsset)(resolution);
3674
+ const academyMode = (0, assetResolver_1.determineAcademyMode)(resolution);
3675
+ const warnings = [...resolution.warnings];
3676
+ if (resolution.packageState === "unknown") {
3677
+ warnings.push(`${resolution.packageError}; existing assets could not be checked.`);
3678
+ }
3679
+ for (const [lang, entry] of resolution.byLang) {
3680
+ if (entry.status === "unknown") {
3681
+ warnings.push(`"${lang}": ${entry.error}`);
3682
+ }
3683
+ else if (entry.status === "ambiguous") {
3684
+ warnings.push(`"${lang}": ${entry.candidates.length} assets found (${entry.candidates
3685
+ .map(c => `#${c.id} ${c.slug}`)
3686
+ .join(", ")})`);
3665
3687
  }
3666
3688
  }
3667
- const uniqueAcademies = [...new Set(foundAcademyIds)];
3668
- let mode;
3669
- let lockedAcademyId;
3670
- let conflictAcademies;
3671
- if (uniqueAcademies.length === 0) {
3672
- mode = "select";
3673
- }
3674
- else if (uniqueAcademies.length === 1) {
3675
- mode = "locked";
3676
- lockedAcademyId = uniqueAcademies[0];
3677
- }
3678
- else {
3679
- mode = "conflict";
3680
- conflictAcademies = uniqueAcademies;
3681
- }
3689
+ const mode = academyMode.type;
3690
+ const lockedAcademyId = academyMode.type === "locked" ? academyMode.academyId : undefined;
3691
+ const conflictAcademies = academyMode.type === "conflict" ? academyMode.academies : undefined;
3682
3692
  return res.json({
3683
3693
  isPublished,
3684
3694
  mode,
3685
3695
  lockedAcademyId,
3686
3696
  conflictAcademies,
3697
+ warnings,
3687
3698
  });
3688
3699
  }
3689
3700
  catch (error) {
@@ -3742,13 +3753,13 @@ class ServeCommand extends SessionCommand_1.default {
3742
3753
  title = config.title[availableLangs[0]];
3743
3754
  selectedLang = availableLangs[0];
3744
3755
  }
3756
+ const previewUrl = await resolveCoursePreviewUrl(bucket, slug, config.preview);
3745
3757
  const idxTpl = fs.readFileSync(path.join(uiSrc, "index.html"), "utf-8");
3746
3758
  const idxHtml = idxTpl
3747
3759
  .replace(/{{title}}/g, title)
3748
3760
  .replace(/<title>.*<\/title>/, `<title>${title}</title>`)
3749
3761
  .replace(/{{description}}/g, config.description[selectedLang])
3750
- .replace(/{{preview}}/g, fixPreviewUrl(slug, "") ||
3751
- "https://raw.githubusercontent.com/learnpack/ide/master/public/learnpack.svg")
3762
+ .replace(/{{preview}}/g, previewUrl)
3752
3763
  .replace(/{{slug}}/g, slug)
3753
3764
  .replace(/{{duration}}/g, (0, misc_1.minutesToISO8601Duration)(config.duration));
3754
3765
  fs.writeFileSync(path.join(buildRoot, "index.html"), idxHtml);
@@ -3778,9 +3789,9 @@ class ServeCommand extends SessionCommand_1.default {
3778
3789
  // 8) Crear el config.json en buildRoot con lo que retorna buildConfig
3779
3790
  const fullConfig = { config, exercises };
3780
3791
  fullConfig.config.slug = slug;
3781
- fullConfig.config.preview =
3782
- fixPreviewUrl(slug, "") ||
3783
- "https://raw.githubusercontent.com/learnpack/ide/master/public/learnpack.svg";
3792
+ // Same URL the meta tags got: this used to overwrite the real preview
3793
+ // of every published package with the unusable SVG fallback.
3794
+ fullConfig.config.preview = previewUrl;
3784
3795
  fs.writeFileSync(path.join(buildRoot, "config.json"), JSON.stringify(fullConfig, null, 2), "utf-8");
3785
3796
  // 9) Empaquetar en ZIP (solo contenido de buildRoot)
3786
3797
  const zipName = `${slug}.zip`;
Binary file
@@ -1,3 +1,4 @@
1
+ export declare const BREATHECODE_HOST = "https://breathecode.herokuapp.com";
1
2
  /**
2
3
  * Student telemetry, and publication events. Both share the endpoint: a body
3
4
  * carrying a top-level `event` key is dispatched as an event, anything else
@@ -31,13 +32,30 @@ export type AssetSyncError = {
31
32
  detail: string;
32
33
  };
33
34
  };
35
+ export type RigoPackageLookup = {
36
+ kind: "found";
37
+ id: number;
38
+ assetIds: number[];
39
+ } | {
40
+ kind: "missing";
41
+ } | {
42
+ kind: "error";
43
+ error: string;
44
+ };
34
45
  /**
35
- * GET Rigobot package by slug after a successful upload. Does not throw.
36
- * @param rigoToken Rigobot API token (Bearer-style `Token` header value).
46
+ * Human-readable summary of a failed axios call, for warnings and error payloads.
47
+ * @param error - Whatever the request rejected with.
48
+ * @returns "HTTP <status>" when the server answered, the error message otherwise.
49
+ */
50
+ export declare function describeRequestError(error: unknown): string;
51
+ /**
52
+ * GET a Rigobot package by slug. Distinguishes "no such package" from
53
+ * "could not check", so callers can fail closed on the latter.
54
+ * @param rigoToken Rigobot API token (`Token` header value).
37
55
  * @param courseSlug Learnpack package slug used in the Rigobot URL path.
38
- * @returns Resolved numeric package id from Rigobot, or `null` if the request fails, the id is missing, or it is not a finite integer.
56
+ * @returns The package id and its asset_ids, `missing` on 404, `error` otherwise (403 included: a package owned by somebody else is not ours to read).
39
57
  */
40
- export declare function resolveLearnpackPackageId(rigoToken: string, courseSlug: string): Promise<number | null>;
58
+ export declare function getRigoPackage(rigoToken: string, courseSlug: string): Promise<RigoPackageLookup>;
41
59
  type TAssetMissing = {
42
60
  slug: string;
43
61
  title: string;
@@ -57,10 +75,41 @@ type TAssetMissing = {
57
75
  learnpack_id: number;
58
76
  };
59
77
  export declare const createAsset: (token: string, asset: TAssetMissing) => Promise<any>;
60
- export declare const doesAssetExists: (token: string, assetSlug: string) => Promise<{
61
- exists: boolean;
62
- academyId?: number;
63
- }>;
78
+ /** The subset of a Breathecode asset the publish flow reads. */
79
+ export type BcAsset = {
80
+ id: number;
81
+ slug: string;
82
+ lang: string | null;
83
+ status: string | null;
84
+ learnpackId: number | null;
85
+ learnpackDeployUrl: string | null;
86
+ ownerId: number | null;
87
+ academyId: number | null;
88
+ };
89
+ export type AssetLookup = {
90
+ kind: "found";
91
+ asset: BcAsset;
92
+ } | {
93
+ kind: "missing";
94
+ } | {
95
+ kind: "error";
96
+ error: string;
97
+ };
98
+ /**
99
+ * GET a Breathecode asset by numeric id. The endpoint applies no status or
100
+ * visibility filter for a single asset, so this is the stable anchor.
101
+ * @param token Breathecode token.
102
+ * @param assetId Asset primary key.
103
+ * @returns `found` with the asset, `missing` on 404, `error` when it could not be checked.
104
+ */
105
+ export declare const getAssetById: (token: string, assetId: number) => Promise<AssetLookup>;
106
+ /**
107
+ * GET a Breathecode asset by slug.
108
+ * @param token Breathecode token.
109
+ * @param assetSlug Asset slug.
110
+ * @returns `found` with the asset, `missing` on 404, `error` when it could not be checked.
111
+ */
112
+ export declare const getAssetBySlug: (token: string, assetSlug: string) => Promise<AssetLookup>;
64
113
  type TTechnology = {
65
114
  slug: string;
66
115
  lang: string;
@@ -83,18 +132,16 @@ declare const _default: {
83
132
  listUserAcademies: (breathecodeToken: string) => Promise<TAcademy[]>;
84
133
  validateToken: (token: string) => Promise<any>;
85
134
  createAsset: (token: string, asset: TAssetMissing) => Promise<any>;
86
- doesAssetExists: (token: string, assetSlug: string) => Promise<{
87
- exists: boolean;
88
- academyId?: number;
89
- }>;
90
- updateAsset: (token: string, assetSlug: string, asset: Partial<TAssetMissing> & {
135
+ getAssetById: (token: string, assetId: number) => Promise<AssetLookup>;
136
+ getAssetBySlug: (token: string, assetSlug: string) => Promise<AssetLookup>;
137
+ updateAsset: (token: string, assetId: number, asset: Partial<TAssetMissing> & {
91
138
  learnpack_id: number;
92
139
  }) => Promise<any>;
93
- resolveLearnpackPackageId: typeof resolveLearnpackPackageId;
140
+ getRigoPackage: typeof getRigoPackage;
94
141
  getCategories: (token: string) => Promise<any>;
95
142
  updateRigoPackage: (token: string, slug: string, updates: {
96
143
  asset_id?: number;
97
- new_slug?: string;
144
+ asset_ids?: number[];
98
145
  }) => Promise<any>;
99
146
  createRigoPackage: (token: string, slug: string, config: any) => Promise<any>;
100
147
  getCurrentTechnologies: () => TTechnology[];