@playcademy/sandbox 0.12.2-beta.6 → 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.
- package/dist/cli.js +31 -14
- package/dist/server.js +31 -14
- 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.
|
|
2520
|
+
version: "0.12.2-beta.7",
|
|
2521
2521
|
description: "Local development server for Playcademy game development",
|
|
2522
2522
|
type: "module",
|
|
2523
2523
|
exports: {
|
|
@@ -20906,11 +20906,14 @@ var init_table8 = __esm(() => {
|
|
|
20906
20906
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
20907
20907
|
name: text("name").notNull(),
|
|
20908
20908
|
clientId: text("client_id").notNull().unique(),
|
|
20909
|
+
handle: text("handle").unique(),
|
|
20909
20910
|
allowedOrigins: text("allowed_origins").array().notNull().default([]),
|
|
20910
20911
|
enabled: boolean("enabled").notNull().default(true),
|
|
20911
20912
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
20912
20913
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
20913
|
-
})
|
|
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
|
+
]);
|
|
20914
20917
|
embedLaunches = pgTable("embed_launches", {
|
|
20915
20918
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
20916
20919
|
partnerId: uuid("partner_id").notNull().references(() => embedPartners.id, { onDelete: "restrict" }),
|
|
@@ -20920,6 +20923,7 @@ var init_table8 = __esm(() => {
|
|
|
20920
20923
|
timebackCourseId: text("timeback_course_id"),
|
|
20921
20924
|
contextId: text("context_id"),
|
|
20922
20925
|
contextRole: embedContextRoleEnum("context_role"),
|
|
20926
|
+
contextAttributes: jsonb("context_attributes").$type(),
|
|
20923
20927
|
passHash: text("pass_hash").unique(),
|
|
20924
20928
|
passExpiresAt: timestamp("pass_expires_at", { withTimezone: true }).notNull(),
|
|
20925
20929
|
redeemedAt: timestamp("redeemed_at", { withTimezone: true }),
|
|
@@ -50827,7 +50831,7 @@ function isCanonicalOrigin(value) {
|
|
|
50827
50831
|
}
|
|
50828
50832
|
return url.protocol === "https:" || url.protocol === "http:" && isLocalHostname(url.hostname);
|
|
50829
50833
|
}
|
|
50830
|
-
var LOCAL_HOSTNAMES, EmbedOriginSchema, EmbedContextIdSchema, EmbedContextSchema, EmbedPartnerClientIdSchema, EnsureEmbedPartnerSchema, AddEmbedPartnerOriginSchema, CreateEmbedLaunchSchema, RedeemEmbedPassSchema;
|
|
50834
|
+
var LOCAL_HOSTNAMES, EmbedOriginSchema, EmbedContextIdSchema, MAX_CONTEXT_ATTRIBUTES = 10, EmbedContextAttributesSchema, EmbedContextSchema, EmbedPartnerClientIdSchema, EnsureEmbedPartnerSchema, AddEmbedPartnerOriginSchema, CreateEmbedLaunchSchema, RedeemEmbedPassSchema;
|
|
50831
50835
|
var init_schemas5 = __esm(() => {
|
|
50832
50836
|
init_esm();
|
|
50833
50837
|
init_table8();
|
|
@@ -50838,9 +50842,17 @@ var init_schemas5 = __esm(() => {
|
|
|
50838
50842
|
EmbedContextIdSchema = stringType().regex(/^[A-Za-z0-9._~-]{1,128}$/, {
|
|
50839
50843
|
message: "context.id must be 1-128 URL-safe characters (letters, digits, . _ ~ -)"
|
|
50840
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
|
+
});
|
|
50841
50852
|
EmbedContextSchema = objectType({
|
|
50842
50853
|
id: EmbedContextIdSchema,
|
|
50843
|
-
role: enumType(embedContextRoleEnum.enumValues).optional()
|
|
50854
|
+
role: enumType(embedContextRoleEnum.enumValues).optional(),
|
|
50855
|
+
attributes: EmbedContextAttributesSchema.optional()
|
|
50844
50856
|
}).strict();
|
|
50845
50857
|
EmbedPartnerClientIdSchema = stringType().trim().min(1).max(200);
|
|
50846
50858
|
EnsureEmbedPartnerSchema = objectType({
|
|
@@ -95699,14 +95711,16 @@ var init_embed_util = __esm(() => {
|
|
|
95699
95711
|
async function scopedContextId(partnerId, contextId) {
|
|
95700
95712
|
return sha256Hex(`${partnerId}:${contextId}`);
|
|
95701
95713
|
}
|
|
95702
|
-
function launchTokenClaims(launch) {
|
|
95714
|
+
function launchTokenClaims(launch, partner) {
|
|
95703
95715
|
if (!launch.contextId) {
|
|
95704
95716
|
return {};
|
|
95705
95717
|
}
|
|
95706
95718
|
return {
|
|
95707
95719
|
context: {
|
|
95708
95720
|
id: launch.contextId,
|
|
95709
|
-
...launch.contextRole && { role: launch.contextRole }
|
|
95721
|
+
...launch.contextRole && { role: launch.contextRole },
|
|
95722
|
+
...launch.contextAttributes && { attributes: launch.contextAttributes },
|
|
95723
|
+
...partner.handle && { partner: partner.handle }
|
|
95710
95724
|
}
|
|
95711
95725
|
};
|
|
95712
95726
|
}
|
|
@@ -95756,6 +95770,7 @@ class EmbedService {
|
|
|
95756
95770
|
timebackCourseId: input.timebackCourseId ?? null,
|
|
95757
95771
|
contextId: input.context ? await scopedContextId(partner.id, input.context.id) : null,
|
|
95758
95772
|
contextRole: input.context?.role ?? null,
|
|
95773
|
+
contextAttributes: input.context?.attributes ?? null,
|
|
95759
95774
|
passHash: await sha256Hex(pass),
|
|
95760
95775
|
passExpiresAt,
|
|
95761
95776
|
expiresAt: new Date(now + EMBED_LAUNCH_TTL_MS)
|
|
@@ -95767,12 +95782,15 @@ class EmbedService {
|
|
|
95767
95782
|
"app.launch.id": launch.id,
|
|
95768
95783
|
"app.user.id": user.id,
|
|
95769
95784
|
"app.embed.has_context": input.context !== undefined,
|
|
95770
|
-
...input.context?.role && { "app.embed.context_role": input.context.role }
|
|
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
|
+
}
|
|
95771
95789
|
});
|
|
95772
95790
|
return { launchId: launch.id, pass, passExpiresAt, gameSlug: game.slug };
|
|
95773
95791
|
}
|
|
95774
95792
|
async redeemLaunch(input, timeZone) {
|
|
95775
|
-
const launch = await this.consumePass(input);
|
|
95793
|
+
const { launch, partner } = await this.consumePass(input);
|
|
95776
95794
|
try {
|
|
95777
95795
|
const user = await this.deps.db.query.users.findFirst({
|
|
95778
95796
|
where: eq(users.id, launch.userId)
|
|
@@ -95783,7 +95801,7 @@ class EmbedService {
|
|
|
95783
95801
|
const [game, profile, token] = await Promise.all([
|
|
95784
95802
|
this.deps.getGameForStudent(launch.gameId, user),
|
|
95785
95803
|
this.deps.getGameProfile(user, launch.gameId, timeZone),
|
|
95786
|
-
this.deps.mintGameToken(launch.gameId, user.id, launchTokenClaims(launch))
|
|
95804
|
+
this.deps.mintGameToken(launch.gameId, user.id, launchTokenClaims(launch, partner))
|
|
95787
95805
|
]);
|
|
95788
95806
|
await this.deps.db.update(embedLaunches).set({ redeemedAt: new Date }).where(eq(embedLaunches.id, launch.id));
|
|
95789
95807
|
return {
|
|
@@ -95815,7 +95833,7 @@ class EmbedService {
|
|
|
95815
95833
|
if (outcome) {
|
|
95816
95834
|
throw new AccessDeniedError("This launch has ended");
|
|
95817
95835
|
}
|
|
95818
|
-
return this.deps.mintGameToken(launch.gameId, launch.userId, launchTokenClaims(launch));
|
|
95836
|
+
return this.deps.mintGameToken(launch.gameId, launch.userId, launchTokenClaims(launch, partner));
|
|
95819
95837
|
}
|
|
95820
95838
|
async endLaunch(launchId) {
|
|
95821
95839
|
await this.deps.db.update(embedLaunches).set({ endedAt: sql`coalesce(${embedLaunches.endedAt}, now())`, passHash: null }).where(eq(embedLaunches.id, launchId));
|
|
@@ -95835,18 +95853,17 @@ class EmbedService {
|
|
|
95835
95853
|
});
|
|
95836
95854
|
const partner = await this.deps.db.query.embedPartners.findFirst({
|
|
95837
95855
|
where: eq(embedPartners.id, launch.partnerId),
|
|
95838
|
-
columns: { enabled: true, allowedOrigins: true }
|
|
95856
|
+
columns: { enabled: true, allowedOrigins: true, handle: true }
|
|
95839
95857
|
});
|
|
95840
95858
|
const originMatches = launch.parentOrigin === input.parentOrigin;
|
|
95841
|
-
|
|
95842
|
-
if (!originMatches || !stillAllowed) {
|
|
95859
|
+
if (!originMatches || !partner || !canPartnerLaunch(partner, launch.parentOrigin)) {
|
|
95843
95860
|
await this.endLaunch(launch.id);
|
|
95844
95861
|
setAttributes({
|
|
95845
95862
|
"app.embed.outcome": originMatches ? "partner_disallowed" : "origin_mismatch"
|
|
95846
95863
|
});
|
|
95847
95864
|
throw new UnauthorizedError("This launch pass is not valid");
|
|
95848
95865
|
}
|
|
95849
|
-
return launch;
|
|
95866
|
+
return { launch, partner };
|
|
95850
95867
|
}
|
|
95851
95868
|
}
|
|
95852
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.
|
|
700
|
+
version: "0.12.2-beta.7",
|
|
701
701
|
description: "Local development server for Playcademy game development",
|
|
702
702
|
type: "module",
|
|
703
703
|
exports: {
|
|
@@ -19086,11 +19086,14 @@ var init_table8 = __esm(() => {
|
|
|
19086
19086
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
19087
19087
|
name: text("name").notNull(),
|
|
19088
19088
|
clientId: text("client_id").notNull().unique(),
|
|
19089
|
+
handle: text("handle").unique(),
|
|
19089
19090
|
allowedOrigins: text("allowed_origins").array().notNull().default([]),
|
|
19090
19091
|
enabled: boolean("enabled").notNull().default(true),
|
|
19091
19092
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
19092
19093
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
19093
|
-
})
|
|
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
|
+
]);
|
|
19094
19097
|
embedLaunches = pgTable("embed_launches", {
|
|
19095
19098
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
19096
19099
|
partnerId: uuid("partner_id").notNull().references(() => embedPartners.id, { onDelete: "restrict" }),
|
|
@@ -19100,6 +19103,7 @@ var init_table8 = __esm(() => {
|
|
|
19100
19103
|
timebackCourseId: text("timeback_course_id"),
|
|
19101
19104
|
contextId: text("context_id"),
|
|
19102
19105
|
contextRole: embedContextRoleEnum("context_role"),
|
|
19106
|
+
contextAttributes: jsonb("context_attributes").$type(),
|
|
19103
19107
|
passHash: text("pass_hash").unique(),
|
|
19104
19108
|
passExpiresAt: timestamp("pass_expires_at", { withTimezone: true }).notNull(),
|
|
19105
19109
|
redeemedAt: timestamp("redeemed_at", { withTimezone: true }),
|
|
@@ -49007,7 +49011,7 @@ function isCanonicalOrigin(value) {
|
|
|
49007
49011
|
}
|
|
49008
49012
|
return url.protocol === "https:" || url.protocol === "http:" && isLocalHostname(url.hostname);
|
|
49009
49013
|
}
|
|
49010
|
-
var LOCAL_HOSTNAMES, EmbedOriginSchema, EmbedContextIdSchema, EmbedContextSchema, EmbedPartnerClientIdSchema, EnsureEmbedPartnerSchema, AddEmbedPartnerOriginSchema, CreateEmbedLaunchSchema, RedeemEmbedPassSchema;
|
|
49014
|
+
var LOCAL_HOSTNAMES, EmbedOriginSchema, EmbedContextIdSchema, MAX_CONTEXT_ATTRIBUTES = 10, EmbedContextAttributesSchema, EmbedContextSchema, EmbedPartnerClientIdSchema, EnsureEmbedPartnerSchema, AddEmbedPartnerOriginSchema, CreateEmbedLaunchSchema, RedeemEmbedPassSchema;
|
|
49011
49015
|
var init_schemas5 = __esm(() => {
|
|
49012
49016
|
init_esm();
|
|
49013
49017
|
init_table8();
|
|
@@ -49018,9 +49022,17 @@ var init_schemas5 = __esm(() => {
|
|
|
49018
49022
|
EmbedContextIdSchema = stringType().regex(/^[A-Za-z0-9._~-]{1,128}$/, {
|
|
49019
49023
|
message: "context.id must be 1-128 URL-safe characters (letters, digits, . _ ~ -)"
|
|
49020
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
|
+
});
|
|
49021
49032
|
EmbedContextSchema = objectType({
|
|
49022
49033
|
id: EmbedContextIdSchema,
|
|
49023
|
-
role: enumType(embedContextRoleEnum.enumValues).optional()
|
|
49034
|
+
role: enumType(embedContextRoleEnum.enumValues).optional(),
|
|
49035
|
+
attributes: EmbedContextAttributesSchema.optional()
|
|
49024
49036
|
}).strict();
|
|
49025
49037
|
EmbedPartnerClientIdSchema = stringType().trim().min(1).max(200);
|
|
49026
49038
|
EnsureEmbedPartnerSchema = objectType({
|
|
@@ -93879,14 +93891,16 @@ var init_embed_util = __esm(() => {
|
|
|
93879
93891
|
async function scopedContextId(partnerId, contextId) {
|
|
93880
93892
|
return sha256Hex(`${partnerId}:${contextId}`);
|
|
93881
93893
|
}
|
|
93882
|
-
function launchTokenClaims(launch) {
|
|
93894
|
+
function launchTokenClaims(launch, partner) {
|
|
93883
93895
|
if (!launch.contextId) {
|
|
93884
93896
|
return {};
|
|
93885
93897
|
}
|
|
93886
93898
|
return {
|
|
93887
93899
|
context: {
|
|
93888
93900
|
id: launch.contextId,
|
|
93889
|
-
...launch.contextRole && { role: launch.contextRole }
|
|
93901
|
+
...launch.contextRole && { role: launch.contextRole },
|
|
93902
|
+
...launch.contextAttributes && { attributes: launch.contextAttributes },
|
|
93903
|
+
...partner.handle && { partner: partner.handle }
|
|
93890
93904
|
}
|
|
93891
93905
|
};
|
|
93892
93906
|
}
|
|
@@ -93936,6 +93950,7 @@ class EmbedService {
|
|
|
93936
93950
|
timebackCourseId: input.timebackCourseId ?? null,
|
|
93937
93951
|
contextId: input.context ? await scopedContextId(partner.id, input.context.id) : null,
|
|
93938
93952
|
contextRole: input.context?.role ?? null,
|
|
93953
|
+
contextAttributes: input.context?.attributes ?? null,
|
|
93939
93954
|
passHash: await sha256Hex(pass),
|
|
93940
93955
|
passExpiresAt,
|
|
93941
93956
|
expiresAt: new Date(now + EMBED_LAUNCH_TTL_MS)
|
|
@@ -93947,12 +93962,15 @@ class EmbedService {
|
|
|
93947
93962
|
"app.launch.id": launch.id,
|
|
93948
93963
|
"app.user.id": user.id,
|
|
93949
93964
|
"app.embed.has_context": input.context !== undefined,
|
|
93950
|
-
...input.context?.role && { "app.embed.context_role": input.context.role }
|
|
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
|
+
}
|
|
93951
93969
|
});
|
|
93952
93970
|
return { launchId: launch.id, pass, passExpiresAt, gameSlug: game.slug };
|
|
93953
93971
|
}
|
|
93954
93972
|
async redeemLaunch(input, timeZone) {
|
|
93955
|
-
const launch = await this.consumePass(input);
|
|
93973
|
+
const { launch, partner } = await this.consumePass(input);
|
|
93956
93974
|
try {
|
|
93957
93975
|
const user = await this.deps.db.query.users.findFirst({
|
|
93958
93976
|
where: eq(users.id, launch.userId)
|
|
@@ -93963,7 +93981,7 @@ class EmbedService {
|
|
|
93963
93981
|
const [game, profile, token] = await Promise.all([
|
|
93964
93982
|
this.deps.getGameForStudent(launch.gameId, user),
|
|
93965
93983
|
this.deps.getGameProfile(user, launch.gameId, timeZone),
|
|
93966
|
-
this.deps.mintGameToken(launch.gameId, user.id, launchTokenClaims(launch))
|
|
93984
|
+
this.deps.mintGameToken(launch.gameId, user.id, launchTokenClaims(launch, partner))
|
|
93967
93985
|
]);
|
|
93968
93986
|
await this.deps.db.update(embedLaunches).set({ redeemedAt: new Date }).where(eq(embedLaunches.id, launch.id));
|
|
93969
93987
|
return {
|
|
@@ -93995,7 +94013,7 @@ class EmbedService {
|
|
|
93995
94013
|
if (outcome) {
|
|
93996
94014
|
throw new AccessDeniedError("This launch has ended");
|
|
93997
94015
|
}
|
|
93998
|
-
return this.deps.mintGameToken(launch.gameId, launch.userId, launchTokenClaims(launch));
|
|
94016
|
+
return this.deps.mintGameToken(launch.gameId, launch.userId, launchTokenClaims(launch, partner));
|
|
93999
94017
|
}
|
|
94000
94018
|
async endLaunch(launchId) {
|
|
94001
94019
|
await this.deps.db.update(embedLaunches).set({ endedAt: sql`coalesce(${embedLaunches.endedAt}, now())`, passHash: null }).where(eq(embedLaunches.id, launchId));
|
|
@@ -94015,18 +94033,17 @@ class EmbedService {
|
|
|
94015
94033
|
});
|
|
94016
94034
|
const partner = await this.deps.db.query.embedPartners.findFirst({
|
|
94017
94035
|
where: eq(embedPartners.id, launch.partnerId),
|
|
94018
|
-
columns: { enabled: true, allowedOrigins: true }
|
|
94036
|
+
columns: { enabled: true, allowedOrigins: true, handle: true }
|
|
94019
94037
|
});
|
|
94020
94038
|
const originMatches = launch.parentOrigin === input.parentOrigin;
|
|
94021
|
-
|
|
94022
|
-
if (!originMatches || !stillAllowed) {
|
|
94039
|
+
if (!originMatches || !partner || !canPartnerLaunch(partner, launch.parentOrigin)) {
|
|
94023
94040
|
await this.endLaunch(launch.id);
|
|
94024
94041
|
setAttributes({
|
|
94025
94042
|
"app.embed.outcome": originMatches ? "partner_disallowed" : "origin_mismatch"
|
|
94026
94043
|
});
|
|
94027
94044
|
throw new UnauthorizedError("This launch pass is not valid");
|
|
94028
94045
|
}
|
|
94029
|
-
return launch;
|
|
94046
|
+
return { launch, partner };
|
|
94030
94047
|
}
|
|
94031
94048
|
}
|
|
94032
94049
|
function renewalRefusal(launch, isAllowed) {
|