@learnpack/learnpack 5.0.355 → 5.0.357

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.
Files changed (120) hide show
  1. package/lib/commands/serve.d.ts +0 -17
  2. package/lib/commands/serve.js +159 -129
  3. package/lib/creatorDist/assets/{index-DnthLsvb.js → index-D6pmbMe9.js} +14030 -13903
  4. package/lib/creatorDist/assets/index-zrPponAn.css +1701 -0
  5. package/lib/creatorDist/index.html +2 -2
  6. package/lib/models/creator.d.ts +1 -1
  7. package/lib/services/ingest/adapters/gcsCourseStorage.d.ts +13 -0
  8. package/lib/services/ingest/adapters/gcsCourseStorage.js +58 -0
  9. package/lib/services/ingest/adapters/rigoUserTokenAuth.d.ts +15 -0
  10. package/lib/services/ingest/adapters/rigoUserTokenAuth.js +46 -0
  11. package/lib/services/ingest/adapters/rigobotPackageRegistry.d.ts +7 -0
  12. package/lib/services/ingest/adapters/rigobotPackageRegistry.js +119 -0
  13. package/lib/services/ingest/core/buildIngestConfig.d.ts +27 -0
  14. package/lib/services/ingest/core/buildIngestConfig.js +40 -0
  15. package/lib/services/ingest/core/buildInitialSyllabus.d.ts +24 -0
  16. package/lib/services/ingest/core/buildInitialSyllabus.js +104 -0
  17. package/lib/services/ingest/core/buildSidebar.d.ts +13 -0
  18. package/lib/services/ingest/core/buildSidebar.js +28 -0
  19. package/lib/services/ingest/core/coursePaths.d.ts +31 -0
  20. package/lib/services/ingest/core/coursePaths.js +50 -0
  21. package/lib/services/ingest/core/ingestCoursePackage.d.ts +52 -0
  22. package/lib/services/ingest/core/ingestCoursePackage.js +217 -0
  23. package/lib/services/ingest/core/normalizeLearnJson.d.ts +25 -0
  24. package/lib/services/ingest/core/normalizeLearnJson.js +29 -0
  25. package/lib/services/ingest/core/pairLessons.d.ts +29 -0
  26. package/lib/services/ingest/core/pairLessons.js +102 -0
  27. package/lib/services/ingest/core/planWrites.d.ts +44 -0
  28. package/lib/services/ingest/core/planWrites.js +59 -0
  29. package/lib/services/ingest/core/resolveSlug.d.ts +53 -0
  30. package/lib/services/ingest/core/resolveSlug.js +93 -0
  31. package/lib/services/ingest/core/types.d.ts +53 -0
  32. package/lib/services/ingest/core/types.js +2 -0
  33. package/lib/services/ingest/core/validatePackage.d.ts +38 -0
  34. package/lib/services/ingest/core/validatePackage.js +97 -0
  35. package/lib/services/ingest/createIngestService.d.ts +34 -0
  36. package/lib/services/ingest/createIngestService.js +38 -0
  37. package/lib/services/ingest/errors.d.ts +22 -0
  38. package/lib/services/ingest/errors.js +34 -0
  39. package/lib/services/ingest/http/errorMapping.d.ts +8 -0
  40. package/lib/services/ingest/http/errorMapping.js +40 -0
  41. package/lib/services/ingest/http/router.d.ts +32 -0
  42. package/lib/services/ingest/http/router.js +124 -0
  43. package/lib/services/ingest/http/zipPackageReader.d.ts +26 -0
  44. package/lib/services/ingest/http/zipPackageReader.js +62 -0
  45. package/lib/services/ingest/ports/courseStorage.d.ts +25 -0
  46. package/lib/services/ingest/ports/courseStorage.js +2 -0
  47. package/lib/services/ingest/ports/packageRegistry.d.ts +58 -0
  48. package/lib/services/ingest/ports/packageRegistry.js +2 -0
  49. package/lib/services/ingest/ports/requestAuthenticator.d.ts +25 -0
  50. package/lib/services/ingest/ports/requestAuthenticator.js +2 -0
  51. package/lib/utils/coursePackage/bucketIo.d.ts +23 -0
  52. package/lib/utils/coursePackage/bucketIo.js +36 -0
  53. package/lib/utils/coursePackage/learnJson.d.ts +26 -0
  54. package/lib/utils/coursePackage/learnJson.js +36 -0
  55. package/lib/utils/coursePackage/sidebar.d.ts +15 -0
  56. package/lib/utils/coursePackage/sidebar.js +26 -0
  57. package/lib/utils/creatorSocket.js +15 -21
  58. package/lib/utils/gcpCredentials.d.ts +18 -0
  59. package/lib/utils/gcpCredentials.js +27 -0
  60. package/lib/utils/rigoActions.d.ts +16 -0
  61. package/lib/utils/rigoActions.js +102 -1
  62. package/lib/utils/socketRegistry.d.ts +46 -0
  63. package/lib/utils/socketRegistry.js +83 -0
  64. package/package.json +1 -1
  65. package/src/commands/serve.ts +162 -114
  66. package/src/creator/README.md +63 -51
  67. package/src/creator/package-lock.json +1188 -8
  68. package/src/creator/package.json +5 -1
  69. package/src/creator/src/App.tsx +120 -54
  70. package/src/creator/src/components/FileUploader.tsx +1 -12
  71. package/src/creator/src/components/NotificationListener.tsx +1 -8
  72. package/src/creator/src/components/syllabus/SyllabusEditor.tsx +101 -36
  73. package/src/creator/src/locales/en.json +8 -0
  74. package/src/creator/src/locales/es.json +8 -0
  75. package/src/creator/src/utils/completionResult.test.ts +144 -0
  76. package/src/creator/src/utils/completionResult.ts +154 -0
  77. package/src/creator/src/utils/constants.ts +17 -4
  78. package/src/creator/src/utils/socket.test.ts +113 -0
  79. package/src/creator/src/utils/socket.ts +60 -37
  80. package/src/creator/src/utils/store.ts +84 -67
  81. package/src/creator/src/utils/useCompletionWatchdog.test.ts +99 -0
  82. package/src/creator/src/utils/useCompletionWatchdog.ts +37 -0
  83. package/src/creator/vitest.config.ts +16 -0
  84. package/src/creatorDist/assets/{index-DnthLsvb.js → index-D6pmbMe9.js} +14030 -13903
  85. package/src/creatorDist/assets/index-zrPponAn.css +1701 -0
  86. package/src/creatorDist/index.html +2 -2
  87. package/src/models/creator.ts +1 -1
  88. package/src/services/ingest/adapters/gcsCourseStorage.ts +70 -0
  89. package/src/services/ingest/adapters/rigoUserTokenAuth.ts +65 -0
  90. package/src/services/ingest/adapters/rigobotPackageRegistry.ts +161 -0
  91. package/src/services/ingest/core/buildIngestConfig.ts +51 -0
  92. package/src/services/ingest/core/buildInitialSyllabus.ts +122 -0
  93. package/src/services/ingest/core/buildSidebar.ts +32 -0
  94. package/src/services/ingest/core/coursePaths.ts +59 -0
  95. package/src/services/ingest/core/ingestCoursePackage.ts +343 -0
  96. package/src/services/ingest/core/normalizeLearnJson.ts +47 -0
  97. package/src/services/ingest/core/pairLessons.ts +132 -0
  98. package/src/services/ingest/core/planWrites.ts +91 -0
  99. package/src/services/ingest/core/resolveSlug.ts +119 -0
  100. package/src/services/ingest/core/types.ts +57 -0
  101. package/src/services/ingest/core/validatePackage.ts +132 -0
  102. package/src/services/ingest/createIngestService.ts +66 -0
  103. package/src/services/ingest/errors.ts +45 -0
  104. package/src/services/ingest/http/errorMapping.ts +49 -0
  105. package/src/services/ingest/http/router.ts +156 -0
  106. package/src/services/ingest/http/zipPackageReader.ts +81 -0
  107. package/src/services/ingest/ports/courseStorage.ts +25 -0
  108. package/src/services/ingest/ports/packageRegistry.ts +60 -0
  109. package/src/services/ingest/ports/requestAuthenticator.ts +28 -0
  110. package/src/ui/_app/app.js +325 -325
  111. package/src/ui/app.tar.gz +0 -0
  112. package/src/utils/coursePackage/bucketIo.ts +46 -0
  113. package/src/utils/coursePackage/learnJson.ts +37 -0
  114. package/src/utils/coursePackage/sidebar.ts +28 -0
  115. package/src/utils/creatorSocket.ts +16 -22
  116. package/src/utils/gcpCredentials.ts +38 -0
  117. package/src/utils/rigoActions.ts +163 -0
  118. package/src/utils/socketRegistry.ts +85 -0
  119. package/lib/creatorDist/assets/index-CjddKHB_.css +0 -1
  120. package/src/creatorDist/assets/index-CjddKHB_.css +0 -1
@@ -0,0 +1,81 @@
1
+ import * as JSZip from "jszip"
2
+ import { IngestError } from "../errors"
3
+ import { PackageFiles } from "../core/validatePackage"
4
+
5
+ /**
6
+ * Rejects an entry whose path could escape the course prefix.
7
+ *
8
+ * Every entry eventually becomes a bucket key under `courses/{slug}/`. A zip is
9
+ * attacker-controlled input, and an entry named `../../other-course/learn.json`
10
+ * would land outside the prefix the caller was authorized for — the storage
11
+ * layer concatenates, it does not resolve.
12
+ * Exported for its own test: JSZip's writer normalizes `..` away, so a zip
13
+ * built through that API cannot express the traversal case, while an archive
14
+ * from any other tool can — its reader passes entry names through untouched.
15
+ * @param path - Entry name as stored in the zip.
16
+ * @returns True when the entry is safe to keep.
17
+ */
18
+ export function isSafeEntryPath(path: string): boolean {
19
+ if (path.startsWith("/") || /^[a-z]:/i.test(path)) {
20
+ return false
21
+ }
22
+
23
+ return !path.split("/").includes("..")
24
+ }
25
+
26
+ /**
27
+ * Reads a course package zip into memory.
28
+ *
29
+ * Lives beside the router rather than in `core/` on purpose: this is decoding a
30
+ * transport format, and the core is meant to know nothing about how a package
31
+ * reached it — not even that it arrived compressed.
32
+ * @param buffer - The uploaded zip.
33
+ * @returns Entry path relative to the package root → text contents.
34
+ * @throws IngestError `INVALID_PACKAGE` when the archive cannot be read.
35
+ */
36
+ export async function readPackageZip(buffer: Buffer): Promise<PackageFiles> {
37
+ let archive: JSZip
38
+
39
+ try {
40
+ archive = await JSZip.loadAsync(buffer)
41
+ } catch (error) {
42
+ throw new IngestError(
43
+ "INVALID_PACKAGE",
44
+ `The uploaded file is not a readable zip: ${(error as Error).message}`,
45
+ error
46
+ )
47
+ }
48
+
49
+ const files: PackageFiles = {}
50
+ const unsafe: string[] = []
51
+
52
+ const entries = Object.values(archive.files).filter(entry => !entry.dir)
53
+
54
+ await Promise.all(
55
+ entries.map(async entry => {
56
+ const path = entry.name.replace(/\\/g, "/")
57
+
58
+ if (!isSafeEntryPath(path)) {
59
+ unsafe.push(entry.name)
60
+ return
61
+ }
62
+
63
+ files[path] = await entry.async("string")
64
+ })
65
+ )
66
+
67
+ if (unsafe.length > 0) {
68
+ throw new IngestError(
69
+ "INVALID_PACKAGE",
70
+ `The zip contains ${unsafe.length} entr${
71
+ unsafe.length === 1 ? "y" : "ies"
72
+ } pointing outside the package: ${unsafe.slice(0, 3).join(", ")}`
73
+ )
74
+ }
75
+
76
+ if (Object.keys(files).length === 0) {
77
+ throw new IngestError("INVALID_PACKAGE", "The zip holds no files")
78
+ }
79
+
80
+ return files
81
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Where an ingested course's files live.
3
+ *
4
+ * Object-level operations only: the ingest core decides *what* the course
5
+ * layout is, and this port only knows how to put bytes somewhere and take them
6
+ * back. Keeping it this small is what lets the core run against an in-memory
7
+ * double in tests, with no credentials and no network.
8
+ */
9
+ export interface CourseStorage {
10
+ /** Writes (or overwrites) a text object at `path`. */
11
+ write(path: string, content: string): Promise<void>;
12
+ /** Reads an object, or null when it does not exist. */
13
+ read(path: string): Promise<string | null>;
14
+ /** Lists every object path under `prefix`. */
15
+ list(prefix: string): Promise<string[]>;
16
+ /**
17
+ * Deletes the given paths.
18
+ *
19
+ * Re-ingesting with `--overwrite` has to remove lessons the regenerated
20
+ * course no longer has: the IDE derives its exercise list from whatever files
21
+ * it finds, so an upsert would leave the old folders behind as phantom
22
+ * lessons.
23
+ */
24
+ deleteMany(paths: string[]): Promise<void>;
25
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * The subset of a Rigobot learning package the ingest service reads.
3
+ *
4
+ * Deliberately not the full payload: naming only what is used keeps the
5
+ * contract honest about what a replacement registry would have to provide.
6
+ */
7
+ export type RigoPackage = {
8
+ id: number;
9
+ package_slug: string;
10
+ /**
11
+ * Organization **id**, or null when Rigobot could not auto-detect one.
12
+ *
13
+ * An id rather than a slug because that is what comes back: the field is a
14
+ * plain ForeignKey on a ModelSerializer, so it serializes to the primary key.
15
+ * The assignment endpoint accepts either form, but nothing ever reads a slug
16
+ * back, which is why ids are the only representation the two ends share.
17
+ */
18
+ organization: number | null;
19
+ status: string;
20
+ };
21
+
22
+ /** One of the organizations the calling user belongs to. */
23
+ export type RigoOrganization = {
24
+ id: number;
25
+ name: string;
26
+ };
27
+
28
+ /**
29
+ * The registry that owns course slugs and their ownership.
30
+ *
31
+ * Slugs are globally unique across every Rigobot user, and in practice
32
+ * immutable — the rename endpoint does not update the package on Rigobot's
33
+ * side — so the ingest resolves availability *before* writing anything.
34
+ */
35
+ export interface PackageRegistry {
36
+ /** True when no package holds this slug yet. */
37
+ isSlugAvailable(slug: string): Promise<boolean>;
38
+ /**
39
+ * Registers a new package.
40
+ * @throws IngestError `SLUG_TAKEN` when the slug is already registered.
41
+ */
42
+ createPackage(slug: string, config: unknown): Promise<RigoPackage>;
43
+ /** Reads a package, or null when it does not exist or is not ours. */
44
+ getPackageBySlug(slug: string): Promise<RigoPackage | null>;
45
+ /**
46
+ * Attaches a package to an organization.
47
+ *
48
+ * One-shot: Rigobot only accepts this while the package's organization is
49
+ * still null, and offers no way to detach it afterwards.
50
+ */
51
+ assignOrganization(packageId: number, organizationId: number): Promise<void>;
52
+ /**
53
+ * The organizations the calling user belongs to.
54
+ *
55
+ * Needed because configuration names an organization the way a human would,
56
+ * while packages report it as an id. This is the only lookup that bridges
57
+ * the two — and it doubles as the list to show when the name matches none.
58
+ */
59
+ listOrganizations(): Promise<RigoOrganization[]>;
60
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Who is making an ingest request.
3
+ *
4
+ * Carried as data through the core, never re-read from headers or the
5
+ * environment further in: the moment the core reaches for a credential itself,
6
+ * swapping the auth scheme stops being a one-file change.
7
+ */
8
+ export type Identity = {
9
+ rigoToken: string;
10
+ };
11
+
12
+ /**
13
+ * Resolves an identity from a request's headers.
14
+ *
15
+ * Takes headers rather than a request object on purpose — the port must not
16
+ * know that Express exists, so the day this service moves behind a different
17
+ * transport only the adapter changes. Today the credential is a Rigobot user
18
+ * token; when the machine-to-machine token arrives, it is a second adapter
19
+ * here and nothing else moves.
20
+ */
21
+ export interface RequestAuthenticator {
22
+ /**
23
+ * @throws IngestError `INVALID_TOKEN` when the credential is missing or rejected.
24
+ */
25
+ authenticate(
26
+ headers: Record<string, string | string[] | undefined>
27
+ ): Promise<Identity>;
28
+ }