@playcademy/sandbox 0.12.2-beta.5 → 0.12.2-beta.7

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 (3) hide show
  1. package/dist/cli.js +53 -18
  2. package/dist/server.js +53 -18
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2517,7 +2517,7 @@ var package_default;
2517
2517
  var init_package = __esm(() => {
2518
2518
  package_default = {
2519
2519
  name: "@playcademy/sandbox",
2520
- version: "0.12.2-beta.5",
2520
+ version: "0.12.2-beta.7",
2521
2521
  description: "Local development server for Playcademy game development",
2522
2522
  type: "module",
2523
2523
  exports: {
@@ -20895,21 +20895,25 @@ var init_table7 = __esm(() => {
20895
20895
  });
20896
20896
 
20897
20897
  // ../data/src/domains/embed/table.ts
20898
- var embedPartners, embedLaunches, embedLaunchesRelations;
20898
+ var embedContextRoleEnum, embedPartners, embedLaunches, embedLaunchesRelations;
20899
20899
  var init_table8 = __esm(() => {
20900
20900
  init_drizzle_orm();
20901
20901
  init_pg_core();
20902
20902
  init_table5();
20903
20903
  init_table3();
20904
+ embedContextRoleEnum = pgEnum("embed_context_role", ["host"]);
20904
20905
  embedPartners = pgTable("embed_partners", {
20905
20906
  id: uuid("id").primaryKey().defaultRandom(),
20906
20907
  name: text("name").notNull(),
20907
20908
  clientId: text("client_id").notNull().unique(),
20909
+ handle: text("handle").unique(),
20908
20910
  allowedOrigins: text("allowed_origins").array().notNull().default([]),
20909
20911
  enabled: boolean("enabled").notNull().default(true),
20910
20912
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
20911
20913
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
20912
- });
20914
+ }, (table) => [
20915
+ check("embed_partners_handle_check", sql`${table.handle} ~ '^[a-z0-9]([a-z0-9-]*[a-z0-9])?([.][a-z0-9]([a-z0-9-]*[a-z0-9])?)+$'`)
20916
+ ]);
20913
20917
  embedLaunches = pgTable("embed_launches", {
20914
20918
  id: uuid("id").primaryKey().defaultRandom(),
20915
20919
  partnerId: uuid("partner_id").notNull().references(() => embedPartners.id, { onDelete: "restrict" }),
@@ -20918,6 +20922,8 @@ var init_table8 = __esm(() => {
20918
20922
  parentOrigin: text("parent_origin").notNull(),
20919
20923
  timebackCourseId: text("timeback_course_id"),
20920
20924
  contextId: text("context_id"),
20925
+ contextRole: embedContextRoleEnum("context_role"),
20926
+ contextAttributes: jsonb("context_attributes").$type(),
20921
20927
  passHash: text("pass_hash").unique(),
20922
20928
  passExpiresAt: timestamp("pass_expires_at", { withTimezone: true }).notNull(),
20923
20929
  redeemedAt: timestamp("redeemed_at", { withTimezone: true }),
@@ -20957,6 +20963,7 @@ __export(exports_tables_index, {
20957
20963
  deploymentProviderEnum: () => deploymentProviderEnum,
20958
20964
  deploymentTargetEnum: () => deploymentTargetEnum,
20959
20965
  developerStatusEnum: () => developerStatusEnum,
20966
+ embedContextRoleEnum: () => embedContextRoleEnum,
20960
20967
  embedLaunches: () => embedLaunches,
20961
20968
  embedLaunchesRelations: () => embedLaunchesRelations,
20962
20969
  embedPartners: () => embedPartners,
@@ -50824,16 +50831,29 @@ function isCanonicalOrigin(value) {
50824
50831
  }
50825
50832
  return url.protocol === "https:" || url.protocol === "http:" && isLocalHostname(url.hostname);
50826
50833
  }
50827
- var LOCAL_HOSTNAMES, EmbedOriginSchema, EmbedContextIdSchema, EmbedPartnerClientIdSchema, EnsureEmbedPartnerSchema, AddEmbedPartnerOriginSchema, CreateEmbedLaunchSchema, RedeemEmbedPassSchema;
50834
+ var LOCAL_HOSTNAMES, EmbedOriginSchema, EmbedContextIdSchema, MAX_CONTEXT_ATTRIBUTES = 10, EmbedContextAttributesSchema, EmbedContextSchema, EmbedPartnerClientIdSchema, EnsureEmbedPartnerSchema, AddEmbedPartnerOriginSchema, CreateEmbedLaunchSchema, RedeemEmbedPassSchema;
50828
50835
  var init_schemas5 = __esm(() => {
50829
50836
  init_esm();
50837
+ init_table8();
50830
50838
  LOCAL_HOSTNAMES = new Set(["localhost", "127.0.0.1"]);
50831
50839
  EmbedOriginSchema = stringType().refine(isCanonicalOrigin, {
50832
50840
  message: "Origin must be a bare https origin like https://example.com, with no path or slash"
50833
50841
  });
50834
50842
  EmbedContextIdSchema = stringType().regex(/^[A-Za-z0-9._~-]{1,128}$/, {
50835
- message: "contextId must be 1-128 URL-safe characters (letters, digits, . _ ~ -)"
50836
- });
50843
+ message: "context.id must be 1-128 URL-safe characters (letters, digits, . _ ~ -)"
50844
+ });
50845
+ EmbedContextAttributesSchema = recordType(stringType().regex(/^[A-Za-z][A-Za-z0-9_]{0,39}$/, {
50846
+ message: "context.attributes keys must be 1-40 characters: a letter, then letters, digits or _"
50847
+ }), stringType().regex(/^[A-Za-z0-9._~-]{1,100}$/, {
50848
+ message: "context.attributes values must be 1-100 URL-safe characters (letters, digits, . _ ~ -)"
50849
+ })).refine((attributes) => Object.keys(attributes).length <= MAX_CONTEXT_ATTRIBUTES, {
50850
+ message: `context.attributes takes at most ${MAX_CONTEXT_ATTRIBUTES} keys`
50851
+ });
50852
+ EmbedContextSchema = objectType({
50853
+ id: EmbedContextIdSchema,
50854
+ role: enumType(embedContextRoleEnum.enumValues).optional(),
50855
+ attributes: EmbedContextAttributesSchema.optional()
50856
+ }).strict();
50837
50857
  EmbedPartnerClientIdSchema = stringType().trim().min(1).max(200);
50838
50858
  EnsureEmbedPartnerSchema = objectType({
50839
50859
  name: stringType().trim().min(1).max(100)
@@ -50846,7 +50866,7 @@ var init_schemas5 = __esm(() => {
50846
50866
  gameSlug: stringType().trim().min(1).max(100),
50847
50867
  parentOrigin: EmbedOriginSchema,
50848
50868
  timebackCourseId: stringType().trim().min(1).max(200).optional(),
50849
- contextId: EmbedContextIdSchema.optional()
50869
+ context: EmbedContextSchema.optional()
50850
50870
  }).strict();
50851
50871
  RedeemEmbedPassSchema = objectType({
50852
50872
  pass: stringType().min(1).max(200),
@@ -95691,8 +95711,18 @@ var init_embed_util = __esm(() => {
95691
95711
  async function scopedContextId(partnerId, contextId) {
95692
95712
  return sha256Hex(`${partnerId}:${contextId}`);
95693
95713
  }
95694
- function launchTokenClaims(launch) {
95695
- return launch.contextId ? { contextId: launch.contextId } : {};
95714
+ function launchTokenClaims(launch, partner) {
95715
+ if (!launch.contextId) {
95716
+ return {};
95717
+ }
95718
+ return {
95719
+ context: {
95720
+ id: launch.contextId,
95721
+ ...launch.contextRole && { role: launch.contextRole },
95722
+ ...launch.contextAttributes && { attributes: launch.contextAttributes },
95723
+ ...partner.handle && { partner: partner.handle }
95724
+ }
95725
+ };
95696
95726
  }
95697
95727
 
95698
95728
  class EmbedService {
@@ -95738,7 +95768,9 @@ class EmbedService {
95738
95768
  gameId: game.id,
95739
95769
  parentOrigin: input.parentOrigin,
95740
95770
  timebackCourseId: input.timebackCourseId ?? null,
95741
- contextId: input.contextId ? await scopedContextId(partner.id, input.contextId) : null,
95771
+ contextId: input.context ? await scopedContextId(partner.id, input.context.id) : null,
95772
+ contextRole: input.context?.role ?? null,
95773
+ contextAttributes: input.context?.attributes ?? null,
95742
95774
  passHash: await sha256Hex(pass),
95743
95775
  passExpiresAt,
95744
95776
  expiresAt: new Date(now + EMBED_LAUNCH_TTL_MS)
@@ -95749,12 +95781,16 @@ class EmbedService {
95749
95781
  setAttributes({
95750
95782
  "app.launch.id": launch.id,
95751
95783
  "app.user.id": user.id,
95752
- "app.embed.has_context": input.contextId !== undefined
95784
+ "app.embed.has_context": input.context !== undefined,
95785
+ ...input.context?.role && { "app.embed.context_role": input.context.role },
95786
+ ...input.context?.attributes && {
95787
+ "app.embed.context_attribute_keys": Object.keys(input.context.attributes).toSorted()
95788
+ }
95753
95789
  });
95754
95790
  return { launchId: launch.id, pass, passExpiresAt, gameSlug: game.slug };
95755
95791
  }
95756
95792
  async redeemLaunch(input, timeZone) {
95757
- const launch = await this.consumePass(input);
95793
+ const { launch, partner } = await this.consumePass(input);
95758
95794
  try {
95759
95795
  const user = await this.deps.db.query.users.findFirst({
95760
95796
  where: eq(users.id, launch.userId)
@@ -95765,7 +95801,7 @@ class EmbedService {
95765
95801
  const [game, profile, token] = await Promise.all([
95766
95802
  this.deps.getGameForStudent(launch.gameId, user),
95767
95803
  this.deps.getGameProfile(user, launch.gameId, timeZone),
95768
- this.deps.mintGameToken(launch.gameId, user.id, launchTokenClaims(launch))
95804
+ this.deps.mintGameToken(launch.gameId, user.id, launchTokenClaims(launch, partner))
95769
95805
  ]);
95770
95806
  await this.deps.db.update(embedLaunches).set({ redeemedAt: new Date }).where(eq(embedLaunches.id, launch.id));
95771
95807
  return {
@@ -95797,7 +95833,7 @@ class EmbedService {
95797
95833
  if (outcome) {
95798
95834
  throw new AccessDeniedError("This launch has ended");
95799
95835
  }
95800
- return this.deps.mintGameToken(launch.gameId, launch.userId, launchTokenClaims(launch));
95836
+ return this.deps.mintGameToken(launch.gameId, launch.userId, launchTokenClaims(launch, partner));
95801
95837
  }
95802
95838
  async endLaunch(launchId) {
95803
95839
  await this.deps.db.update(embedLaunches).set({ endedAt: sql`coalesce(${embedLaunches.endedAt}, now())`, passHash: null }).where(eq(embedLaunches.id, launchId));
@@ -95817,18 +95853,17 @@ class EmbedService {
95817
95853
  });
95818
95854
  const partner = await this.deps.db.query.embedPartners.findFirst({
95819
95855
  where: eq(embedPartners.id, launch.partnerId),
95820
- columns: { enabled: true, allowedOrigins: true }
95856
+ columns: { enabled: true, allowedOrigins: true, handle: true }
95821
95857
  });
95822
95858
  const originMatches = launch.parentOrigin === input.parentOrigin;
95823
- const stillAllowed = partner !== undefined && canPartnerLaunch(partner, launch.parentOrigin);
95824
- if (!originMatches || !stillAllowed) {
95859
+ if (!originMatches || !partner || !canPartnerLaunch(partner, launch.parentOrigin)) {
95825
95860
  await this.endLaunch(launch.id);
95826
95861
  setAttributes({
95827
95862
  "app.embed.outcome": originMatches ? "partner_disallowed" : "origin_mismatch"
95828
95863
  });
95829
95864
  throw new UnauthorizedError("This launch pass is not valid");
95830
95865
  }
95831
- return launch;
95866
+ return { launch, partner };
95832
95867
  }
95833
95868
  }
95834
95869
  function renewalRefusal(launch, isAllowed) {
package/dist/server.js CHANGED
@@ -697,7 +697,7 @@ var package_default;
697
697
  var init_package = __esm(() => {
698
698
  package_default = {
699
699
  name: "@playcademy/sandbox",
700
- version: "0.12.2-beta.5",
700
+ version: "0.12.2-beta.7",
701
701
  description: "Local development server for Playcademy game development",
702
702
  type: "module",
703
703
  exports: {
@@ -19075,21 +19075,25 @@ var init_table7 = __esm(() => {
19075
19075
  });
19076
19076
 
19077
19077
  // ../data/src/domains/embed/table.ts
19078
- var embedPartners, embedLaunches, embedLaunchesRelations;
19078
+ var embedContextRoleEnum, embedPartners, embedLaunches, embedLaunchesRelations;
19079
19079
  var init_table8 = __esm(() => {
19080
19080
  init_drizzle_orm();
19081
19081
  init_pg_core();
19082
19082
  init_table5();
19083
19083
  init_table3();
19084
+ embedContextRoleEnum = pgEnum("embed_context_role", ["host"]);
19084
19085
  embedPartners = pgTable("embed_partners", {
19085
19086
  id: uuid("id").primaryKey().defaultRandom(),
19086
19087
  name: text("name").notNull(),
19087
19088
  clientId: text("client_id").notNull().unique(),
19089
+ handle: text("handle").unique(),
19088
19090
  allowedOrigins: text("allowed_origins").array().notNull().default([]),
19089
19091
  enabled: boolean("enabled").notNull().default(true),
19090
19092
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
19091
19093
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
19092
- });
19094
+ }, (table) => [
19095
+ check("embed_partners_handle_check", sql`${table.handle} ~ '^[a-z0-9]([a-z0-9-]*[a-z0-9])?([.][a-z0-9]([a-z0-9-]*[a-z0-9])?)+$'`)
19096
+ ]);
19093
19097
  embedLaunches = pgTable("embed_launches", {
19094
19098
  id: uuid("id").primaryKey().defaultRandom(),
19095
19099
  partnerId: uuid("partner_id").notNull().references(() => embedPartners.id, { onDelete: "restrict" }),
@@ -19098,6 +19102,8 @@ var init_table8 = __esm(() => {
19098
19102
  parentOrigin: text("parent_origin").notNull(),
19099
19103
  timebackCourseId: text("timeback_course_id"),
19100
19104
  contextId: text("context_id"),
19105
+ contextRole: embedContextRoleEnum("context_role"),
19106
+ contextAttributes: jsonb("context_attributes").$type(),
19101
19107
  passHash: text("pass_hash").unique(),
19102
19108
  passExpiresAt: timestamp("pass_expires_at", { withTimezone: true }).notNull(),
19103
19109
  redeemedAt: timestamp("redeemed_at", { withTimezone: true }),
@@ -19137,6 +19143,7 @@ __export(exports_tables_index, {
19137
19143
  deploymentProviderEnum: () => deploymentProviderEnum,
19138
19144
  deploymentTargetEnum: () => deploymentTargetEnum,
19139
19145
  developerStatusEnum: () => developerStatusEnum,
19146
+ embedContextRoleEnum: () => embedContextRoleEnum,
19140
19147
  embedLaunches: () => embedLaunches,
19141
19148
  embedLaunchesRelations: () => embedLaunchesRelations,
19142
19149
  embedPartners: () => embedPartners,
@@ -49004,16 +49011,29 @@ function isCanonicalOrigin(value) {
49004
49011
  }
49005
49012
  return url.protocol === "https:" || url.protocol === "http:" && isLocalHostname(url.hostname);
49006
49013
  }
49007
- var LOCAL_HOSTNAMES, EmbedOriginSchema, EmbedContextIdSchema, EmbedPartnerClientIdSchema, EnsureEmbedPartnerSchema, AddEmbedPartnerOriginSchema, CreateEmbedLaunchSchema, RedeemEmbedPassSchema;
49014
+ var LOCAL_HOSTNAMES, EmbedOriginSchema, EmbedContextIdSchema, MAX_CONTEXT_ATTRIBUTES = 10, EmbedContextAttributesSchema, EmbedContextSchema, EmbedPartnerClientIdSchema, EnsureEmbedPartnerSchema, AddEmbedPartnerOriginSchema, CreateEmbedLaunchSchema, RedeemEmbedPassSchema;
49008
49015
  var init_schemas5 = __esm(() => {
49009
49016
  init_esm();
49017
+ init_table8();
49010
49018
  LOCAL_HOSTNAMES = new Set(["localhost", "127.0.0.1"]);
49011
49019
  EmbedOriginSchema = stringType().refine(isCanonicalOrigin, {
49012
49020
  message: "Origin must be a bare https origin like https://example.com, with no path or slash"
49013
49021
  });
49014
49022
  EmbedContextIdSchema = stringType().regex(/^[A-Za-z0-9._~-]{1,128}$/, {
49015
- message: "contextId must be 1-128 URL-safe characters (letters, digits, . _ ~ -)"
49016
- });
49023
+ message: "context.id must be 1-128 URL-safe characters (letters, digits, . _ ~ -)"
49024
+ });
49025
+ EmbedContextAttributesSchema = recordType(stringType().regex(/^[A-Za-z][A-Za-z0-9_]{0,39}$/, {
49026
+ message: "context.attributes keys must be 1-40 characters: a letter, then letters, digits or _"
49027
+ }), stringType().regex(/^[A-Za-z0-9._~-]{1,100}$/, {
49028
+ message: "context.attributes values must be 1-100 URL-safe characters (letters, digits, . _ ~ -)"
49029
+ })).refine((attributes) => Object.keys(attributes).length <= MAX_CONTEXT_ATTRIBUTES, {
49030
+ message: `context.attributes takes at most ${MAX_CONTEXT_ATTRIBUTES} keys`
49031
+ });
49032
+ EmbedContextSchema = objectType({
49033
+ id: EmbedContextIdSchema,
49034
+ role: enumType(embedContextRoleEnum.enumValues).optional(),
49035
+ attributes: EmbedContextAttributesSchema.optional()
49036
+ }).strict();
49017
49037
  EmbedPartnerClientIdSchema = stringType().trim().min(1).max(200);
49018
49038
  EnsureEmbedPartnerSchema = objectType({
49019
49039
  name: stringType().trim().min(1).max(100)
@@ -49026,7 +49046,7 @@ var init_schemas5 = __esm(() => {
49026
49046
  gameSlug: stringType().trim().min(1).max(100),
49027
49047
  parentOrigin: EmbedOriginSchema,
49028
49048
  timebackCourseId: stringType().trim().min(1).max(200).optional(),
49029
- contextId: EmbedContextIdSchema.optional()
49049
+ context: EmbedContextSchema.optional()
49030
49050
  }).strict();
49031
49051
  RedeemEmbedPassSchema = objectType({
49032
49052
  pass: stringType().min(1).max(200),
@@ -93871,8 +93891,18 @@ var init_embed_util = __esm(() => {
93871
93891
  async function scopedContextId(partnerId, contextId) {
93872
93892
  return sha256Hex(`${partnerId}:${contextId}`);
93873
93893
  }
93874
- function launchTokenClaims(launch) {
93875
- return launch.contextId ? { contextId: launch.contextId } : {};
93894
+ function launchTokenClaims(launch, partner) {
93895
+ if (!launch.contextId) {
93896
+ return {};
93897
+ }
93898
+ return {
93899
+ context: {
93900
+ id: launch.contextId,
93901
+ ...launch.contextRole && { role: launch.contextRole },
93902
+ ...launch.contextAttributes && { attributes: launch.contextAttributes },
93903
+ ...partner.handle && { partner: partner.handle }
93904
+ }
93905
+ };
93876
93906
  }
93877
93907
 
93878
93908
  class EmbedService {
@@ -93918,7 +93948,9 @@ class EmbedService {
93918
93948
  gameId: game.id,
93919
93949
  parentOrigin: input.parentOrigin,
93920
93950
  timebackCourseId: input.timebackCourseId ?? null,
93921
- contextId: input.contextId ? await scopedContextId(partner.id, input.contextId) : null,
93951
+ contextId: input.context ? await scopedContextId(partner.id, input.context.id) : null,
93952
+ contextRole: input.context?.role ?? null,
93953
+ contextAttributes: input.context?.attributes ?? null,
93922
93954
  passHash: await sha256Hex(pass),
93923
93955
  passExpiresAt,
93924
93956
  expiresAt: new Date(now + EMBED_LAUNCH_TTL_MS)
@@ -93929,12 +93961,16 @@ class EmbedService {
93929
93961
  setAttributes({
93930
93962
  "app.launch.id": launch.id,
93931
93963
  "app.user.id": user.id,
93932
- "app.embed.has_context": input.contextId !== undefined
93964
+ "app.embed.has_context": input.context !== undefined,
93965
+ ...input.context?.role && { "app.embed.context_role": input.context.role },
93966
+ ...input.context?.attributes && {
93967
+ "app.embed.context_attribute_keys": Object.keys(input.context.attributes).toSorted()
93968
+ }
93933
93969
  });
93934
93970
  return { launchId: launch.id, pass, passExpiresAt, gameSlug: game.slug };
93935
93971
  }
93936
93972
  async redeemLaunch(input, timeZone) {
93937
- const launch = await this.consumePass(input);
93973
+ const { launch, partner } = await this.consumePass(input);
93938
93974
  try {
93939
93975
  const user = await this.deps.db.query.users.findFirst({
93940
93976
  where: eq(users.id, launch.userId)
@@ -93945,7 +93981,7 @@ class EmbedService {
93945
93981
  const [game, profile, token] = await Promise.all([
93946
93982
  this.deps.getGameForStudent(launch.gameId, user),
93947
93983
  this.deps.getGameProfile(user, launch.gameId, timeZone),
93948
- this.deps.mintGameToken(launch.gameId, user.id, launchTokenClaims(launch))
93984
+ this.deps.mintGameToken(launch.gameId, user.id, launchTokenClaims(launch, partner))
93949
93985
  ]);
93950
93986
  await this.deps.db.update(embedLaunches).set({ redeemedAt: new Date }).where(eq(embedLaunches.id, launch.id));
93951
93987
  return {
@@ -93977,7 +94013,7 @@ class EmbedService {
93977
94013
  if (outcome) {
93978
94014
  throw new AccessDeniedError("This launch has ended");
93979
94015
  }
93980
- return this.deps.mintGameToken(launch.gameId, launch.userId, launchTokenClaims(launch));
94016
+ return this.deps.mintGameToken(launch.gameId, launch.userId, launchTokenClaims(launch, partner));
93981
94017
  }
93982
94018
  async endLaunch(launchId) {
93983
94019
  await this.deps.db.update(embedLaunches).set({ endedAt: sql`coalesce(${embedLaunches.endedAt}, now())`, passHash: null }).where(eq(embedLaunches.id, launchId));
@@ -93997,18 +94033,17 @@ class EmbedService {
93997
94033
  });
93998
94034
  const partner = await this.deps.db.query.embedPartners.findFirst({
93999
94035
  where: eq(embedPartners.id, launch.partnerId),
94000
- columns: { enabled: true, allowedOrigins: true }
94036
+ columns: { enabled: true, allowedOrigins: true, handle: true }
94001
94037
  });
94002
94038
  const originMatches = launch.parentOrigin === input.parentOrigin;
94003
- const stillAllowed = partner !== undefined && canPartnerLaunch(partner, launch.parentOrigin);
94004
- if (!originMatches || !stillAllowed) {
94039
+ if (!originMatches || !partner || !canPartnerLaunch(partner, launch.parentOrigin)) {
94005
94040
  await this.endLaunch(launch.id);
94006
94041
  setAttributes({
94007
94042
  "app.embed.outcome": originMatches ? "partner_disallowed" : "origin_mismatch"
94008
94043
  });
94009
94044
  throw new UnauthorizedError("This launch pass is not valid");
94010
94045
  }
94011
- return launch;
94046
+ return { launch, partner };
94012
94047
  }
94013
94048
  }
94014
94049
  function renewalRefusal(launch, isAllowed) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcademy/sandbox",
3
- "version": "0.12.2-beta.5",
3
+ "version": "0.12.2-beta.7",
4
4
  "description": "Local development server for Playcademy game development",
5
5
  "type": "module",
6
6
  "exports": {