@playcademy/sandbox 0.12.2-beta.6 → 0.13.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/dist/cli.js +55 -23
- package/dist/server.js +55 -23
- 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.
|
|
2520
|
+
version: "0.13.0",
|
|
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, EmbedPartnerHandleSchema, EnsureEmbedPartnerSchema, AddEmbedPartnerOriginSchema, CreateEmbedLaunchSchema, RedeemEmbedPassSchema;
|
|
50831
50835
|
var init_schemas5 = __esm(() => {
|
|
50832
50836
|
init_esm();
|
|
50833
50837
|
init_table8();
|
|
@@ -50838,13 +50842,23 @@ 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);
|
|
50858
|
+
EmbedPartnerHandleSchema = stringType().max(253).regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?([.][a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/);
|
|
50846
50859
|
EnsureEmbedPartnerSchema = objectType({
|
|
50847
|
-
name: stringType().trim().min(1).max(100)
|
|
50860
|
+
name: stringType().trim().min(1).max(100),
|
|
50861
|
+
handle: EmbedPartnerHandleSchema.optional()
|
|
50848
50862
|
}).strict();
|
|
50849
50863
|
AddEmbedPartnerOriginSchema = objectType({
|
|
50850
50864
|
origin: EmbedOriginSchema
|
|
@@ -95650,15 +95664,28 @@ class EmbedPartnerService {
|
|
|
95650
95664
|
constructor(deps) {
|
|
95651
95665
|
this.deps = deps;
|
|
95652
95666
|
}
|
|
95653
|
-
async ensurePartner(clientId, name2) {
|
|
95654
|
-
const [partner] = await this.deps.db.insert(embedPartners).values({ clientId, name: name2 }).onConflictDoUpdate({
|
|
95667
|
+
async ensurePartner(clientId, name2, handle2) {
|
|
95668
|
+
const [partner] = await this.deps.db.insert(embedPartners).values({ clientId, name: name2, handle: handle2 }).onConflictDoUpdate({
|
|
95655
95669
|
target: embedPartners.clientId,
|
|
95656
|
-
set: {
|
|
95657
|
-
|
|
95658
|
-
|
|
95659
|
-
|
|
95670
|
+
set: {
|
|
95671
|
+
name: name2,
|
|
95672
|
+
handle: sql`coalesce(${embedPartners.handle}, excluded.handle)`,
|
|
95673
|
+
updatedAt: new Date
|
|
95674
|
+
},
|
|
95675
|
+
setWhere: sql`excluded.handle IS NULL OR ${embedPartners.handle} IS NULL OR ${embedPartners.handle} = excluded.handle`
|
|
95676
|
+
}).returning().catch((error) => {
|
|
95677
|
+
if (isUniqueViolation(error)) {
|
|
95678
|
+
throw new ConflictError(`Another embed partner has the handle ${handle2}`);
|
|
95679
|
+
}
|
|
95680
|
+
throw error;
|
|
95681
|
+
});
|
|
95682
|
+
if (partner) {
|
|
95683
|
+
return partner;
|
|
95660
95684
|
}
|
|
95661
|
-
|
|
95685
|
+
if (handle2) {
|
|
95686
|
+
throw new ConflictError("This embed partner already has a different handle, and it never changes");
|
|
95687
|
+
}
|
|
95688
|
+
throw new InternalError("Failed to ensure embed partner");
|
|
95662
95689
|
}
|
|
95663
95690
|
addOrigin(clientId, origin) {
|
|
95664
95691
|
return this.setOrigins(clientId, sql`CASE WHEN ${origin} = ANY(${embedPartners.allowedOrigins})
|
|
@@ -95699,14 +95726,16 @@ var init_embed_util = __esm(() => {
|
|
|
95699
95726
|
async function scopedContextId(partnerId, contextId) {
|
|
95700
95727
|
return sha256Hex(`${partnerId}:${contextId}`);
|
|
95701
95728
|
}
|
|
95702
|
-
function launchTokenClaims(launch) {
|
|
95729
|
+
function launchTokenClaims(launch, partner) {
|
|
95703
95730
|
if (!launch.contextId) {
|
|
95704
95731
|
return {};
|
|
95705
95732
|
}
|
|
95706
95733
|
return {
|
|
95707
95734
|
context: {
|
|
95708
95735
|
id: launch.contextId,
|
|
95709
|
-
...launch.contextRole && { role: launch.contextRole }
|
|
95736
|
+
...launch.contextRole && { role: launch.contextRole },
|
|
95737
|
+
...launch.contextAttributes && { attributes: launch.contextAttributes },
|
|
95738
|
+
...partner.handle && { partner: partner.handle }
|
|
95710
95739
|
}
|
|
95711
95740
|
};
|
|
95712
95741
|
}
|
|
@@ -95756,6 +95785,7 @@ class EmbedService {
|
|
|
95756
95785
|
timebackCourseId: input.timebackCourseId ?? null,
|
|
95757
95786
|
contextId: input.context ? await scopedContextId(partner.id, input.context.id) : null,
|
|
95758
95787
|
contextRole: input.context?.role ?? null,
|
|
95788
|
+
contextAttributes: input.context?.attributes ?? null,
|
|
95759
95789
|
passHash: await sha256Hex(pass),
|
|
95760
95790
|
passExpiresAt,
|
|
95761
95791
|
expiresAt: new Date(now + EMBED_LAUNCH_TTL_MS)
|
|
@@ -95767,12 +95797,15 @@ class EmbedService {
|
|
|
95767
95797
|
"app.launch.id": launch.id,
|
|
95768
95798
|
"app.user.id": user.id,
|
|
95769
95799
|
"app.embed.has_context": input.context !== undefined,
|
|
95770
|
-
...input.context?.role && { "app.embed.context_role": input.context.role }
|
|
95800
|
+
...input.context?.role && { "app.embed.context_role": input.context.role },
|
|
95801
|
+
...input.context?.attributes && {
|
|
95802
|
+
"app.embed.context_attribute_keys": Object.keys(input.context.attributes).toSorted()
|
|
95803
|
+
}
|
|
95771
95804
|
});
|
|
95772
95805
|
return { launchId: launch.id, pass, passExpiresAt, gameSlug: game.slug };
|
|
95773
95806
|
}
|
|
95774
95807
|
async redeemLaunch(input, timeZone) {
|
|
95775
|
-
const launch = await this.consumePass(input);
|
|
95808
|
+
const { launch, partner } = await this.consumePass(input);
|
|
95776
95809
|
try {
|
|
95777
95810
|
const user = await this.deps.db.query.users.findFirst({
|
|
95778
95811
|
where: eq(users.id, launch.userId)
|
|
@@ -95783,7 +95816,7 @@ class EmbedService {
|
|
|
95783
95816
|
const [game, profile, token] = await Promise.all([
|
|
95784
95817
|
this.deps.getGameForStudent(launch.gameId, user),
|
|
95785
95818
|
this.deps.getGameProfile(user, launch.gameId, timeZone),
|
|
95786
|
-
this.deps.mintGameToken(launch.gameId, user.id, launchTokenClaims(launch))
|
|
95819
|
+
this.deps.mintGameToken(launch.gameId, user.id, launchTokenClaims(launch, partner))
|
|
95787
95820
|
]);
|
|
95788
95821
|
await this.deps.db.update(embedLaunches).set({ redeemedAt: new Date }).where(eq(embedLaunches.id, launch.id));
|
|
95789
95822
|
return {
|
|
@@ -95815,7 +95848,7 @@ class EmbedService {
|
|
|
95815
95848
|
if (outcome) {
|
|
95816
95849
|
throw new AccessDeniedError("This launch has ended");
|
|
95817
95850
|
}
|
|
95818
|
-
return this.deps.mintGameToken(launch.gameId, launch.userId, launchTokenClaims(launch));
|
|
95851
|
+
return this.deps.mintGameToken(launch.gameId, launch.userId, launchTokenClaims(launch, partner));
|
|
95819
95852
|
}
|
|
95820
95853
|
async endLaunch(launchId) {
|
|
95821
95854
|
await this.deps.db.update(embedLaunches).set({ endedAt: sql`coalesce(${embedLaunches.endedAt}, now())`, passHash: null }).where(eq(embedLaunches.id, launchId));
|
|
@@ -95835,18 +95868,17 @@ class EmbedService {
|
|
|
95835
95868
|
});
|
|
95836
95869
|
const partner = await this.deps.db.query.embedPartners.findFirst({
|
|
95837
95870
|
where: eq(embedPartners.id, launch.partnerId),
|
|
95838
|
-
columns: { enabled: true, allowedOrigins: true }
|
|
95871
|
+
columns: { enabled: true, allowedOrigins: true, handle: true }
|
|
95839
95872
|
});
|
|
95840
95873
|
const originMatches = launch.parentOrigin === input.parentOrigin;
|
|
95841
|
-
|
|
95842
|
-
if (!originMatches || !stillAllowed) {
|
|
95874
|
+
if (!originMatches || !partner || !canPartnerLaunch(partner, launch.parentOrigin)) {
|
|
95843
95875
|
await this.endLaunch(launch.id);
|
|
95844
95876
|
setAttributes({
|
|
95845
95877
|
"app.embed.outcome": originMatches ? "partner_disallowed" : "origin_mismatch"
|
|
95846
95878
|
});
|
|
95847
95879
|
throw new UnauthorizedError("This launch pass is not valid");
|
|
95848
95880
|
}
|
|
95849
|
-
return launch;
|
|
95881
|
+
return { launch, partner };
|
|
95850
95882
|
}
|
|
95851
95883
|
}
|
|
95852
95884
|
function renewalRefusal(launch, isAllowed) {
|
|
@@ -152662,7 +152694,7 @@ async function ensurePartner(ctx) {
|
|
|
152662
152694
|
await requireEmbedAdmin(ctx);
|
|
152663
152695
|
const clientId = requireClientIdParam(ctx);
|
|
152664
152696
|
const body2 = await parseRequestBody(ctx.request, EnsureEmbedPartnerSchema);
|
|
152665
|
-
return ctx.services.embedPartner.ensurePartner(clientId, body2.name);
|
|
152697
|
+
return ctx.services.embedPartner.ensurePartner(clientId, body2.name, body2.handle);
|
|
152666
152698
|
}
|
|
152667
152699
|
async function addPartnerOrigin(ctx) {
|
|
152668
152700
|
await requireEmbedAdmin(ctx);
|
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.
|
|
700
|
+
version: "0.13.0",
|
|
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, EmbedPartnerHandleSchema, EnsureEmbedPartnerSchema, AddEmbedPartnerOriginSchema, CreateEmbedLaunchSchema, RedeemEmbedPassSchema;
|
|
49011
49015
|
var init_schemas5 = __esm(() => {
|
|
49012
49016
|
init_esm();
|
|
49013
49017
|
init_table8();
|
|
@@ -49018,13 +49022,23 @@ 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);
|
|
49038
|
+
EmbedPartnerHandleSchema = stringType().max(253).regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?([.][a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/);
|
|
49026
49039
|
EnsureEmbedPartnerSchema = objectType({
|
|
49027
|
-
name: stringType().trim().min(1).max(100)
|
|
49040
|
+
name: stringType().trim().min(1).max(100),
|
|
49041
|
+
handle: EmbedPartnerHandleSchema.optional()
|
|
49028
49042
|
}).strict();
|
|
49029
49043
|
AddEmbedPartnerOriginSchema = objectType({
|
|
49030
49044
|
origin: EmbedOriginSchema
|
|
@@ -93830,15 +93844,28 @@ class EmbedPartnerService {
|
|
|
93830
93844
|
constructor(deps) {
|
|
93831
93845
|
this.deps = deps;
|
|
93832
93846
|
}
|
|
93833
|
-
async ensurePartner(clientId, name2) {
|
|
93834
|
-
const [partner] = await this.deps.db.insert(embedPartners).values({ clientId, name: name2 }).onConflictDoUpdate({
|
|
93847
|
+
async ensurePartner(clientId, name2, handle2) {
|
|
93848
|
+
const [partner] = await this.deps.db.insert(embedPartners).values({ clientId, name: name2, handle: handle2 }).onConflictDoUpdate({
|
|
93835
93849
|
target: embedPartners.clientId,
|
|
93836
|
-
set: {
|
|
93837
|
-
|
|
93838
|
-
|
|
93839
|
-
|
|
93850
|
+
set: {
|
|
93851
|
+
name: name2,
|
|
93852
|
+
handle: sql`coalesce(${embedPartners.handle}, excluded.handle)`,
|
|
93853
|
+
updatedAt: new Date
|
|
93854
|
+
},
|
|
93855
|
+
setWhere: sql`excluded.handle IS NULL OR ${embedPartners.handle} IS NULL OR ${embedPartners.handle} = excluded.handle`
|
|
93856
|
+
}).returning().catch((error) => {
|
|
93857
|
+
if (isUniqueViolation(error)) {
|
|
93858
|
+
throw new ConflictError(`Another embed partner has the handle ${handle2}`);
|
|
93859
|
+
}
|
|
93860
|
+
throw error;
|
|
93861
|
+
});
|
|
93862
|
+
if (partner) {
|
|
93863
|
+
return partner;
|
|
93840
93864
|
}
|
|
93841
|
-
|
|
93865
|
+
if (handle2) {
|
|
93866
|
+
throw new ConflictError("This embed partner already has a different handle, and it never changes");
|
|
93867
|
+
}
|
|
93868
|
+
throw new InternalError("Failed to ensure embed partner");
|
|
93842
93869
|
}
|
|
93843
93870
|
addOrigin(clientId, origin) {
|
|
93844
93871
|
return this.setOrigins(clientId, sql`CASE WHEN ${origin} = ANY(${embedPartners.allowedOrigins})
|
|
@@ -93879,14 +93906,16 @@ var init_embed_util = __esm(() => {
|
|
|
93879
93906
|
async function scopedContextId(partnerId, contextId) {
|
|
93880
93907
|
return sha256Hex(`${partnerId}:${contextId}`);
|
|
93881
93908
|
}
|
|
93882
|
-
function launchTokenClaims(launch) {
|
|
93909
|
+
function launchTokenClaims(launch, partner) {
|
|
93883
93910
|
if (!launch.contextId) {
|
|
93884
93911
|
return {};
|
|
93885
93912
|
}
|
|
93886
93913
|
return {
|
|
93887
93914
|
context: {
|
|
93888
93915
|
id: launch.contextId,
|
|
93889
|
-
...launch.contextRole && { role: launch.contextRole }
|
|
93916
|
+
...launch.contextRole && { role: launch.contextRole },
|
|
93917
|
+
...launch.contextAttributes && { attributes: launch.contextAttributes },
|
|
93918
|
+
...partner.handle && { partner: partner.handle }
|
|
93890
93919
|
}
|
|
93891
93920
|
};
|
|
93892
93921
|
}
|
|
@@ -93936,6 +93965,7 @@ class EmbedService {
|
|
|
93936
93965
|
timebackCourseId: input.timebackCourseId ?? null,
|
|
93937
93966
|
contextId: input.context ? await scopedContextId(partner.id, input.context.id) : null,
|
|
93938
93967
|
contextRole: input.context?.role ?? null,
|
|
93968
|
+
contextAttributes: input.context?.attributes ?? null,
|
|
93939
93969
|
passHash: await sha256Hex(pass),
|
|
93940
93970
|
passExpiresAt,
|
|
93941
93971
|
expiresAt: new Date(now + EMBED_LAUNCH_TTL_MS)
|
|
@@ -93947,12 +93977,15 @@ class EmbedService {
|
|
|
93947
93977
|
"app.launch.id": launch.id,
|
|
93948
93978
|
"app.user.id": user.id,
|
|
93949
93979
|
"app.embed.has_context": input.context !== undefined,
|
|
93950
|
-
...input.context?.role && { "app.embed.context_role": input.context.role }
|
|
93980
|
+
...input.context?.role && { "app.embed.context_role": input.context.role },
|
|
93981
|
+
...input.context?.attributes && {
|
|
93982
|
+
"app.embed.context_attribute_keys": Object.keys(input.context.attributes).toSorted()
|
|
93983
|
+
}
|
|
93951
93984
|
});
|
|
93952
93985
|
return { launchId: launch.id, pass, passExpiresAt, gameSlug: game.slug };
|
|
93953
93986
|
}
|
|
93954
93987
|
async redeemLaunch(input, timeZone) {
|
|
93955
|
-
const launch = await this.consumePass(input);
|
|
93988
|
+
const { launch, partner } = await this.consumePass(input);
|
|
93956
93989
|
try {
|
|
93957
93990
|
const user = await this.deps.db.query.users.findFirst({
|
|
93958
93991
|
where: eq(users.id, launch.userId)
|
|
@@ -93963,7 +93996,7 @@ class EmbedService {
|
|
|
93963
93996
|
const [game, profile, token] = await Promise.all([
|
|
93964
93997
|
this.deps.getGameForStudent(launch.gameId, user),
|
|
93965
93998
|
this.deps.getGameProfile(user, launch.gameId, timeZone),
|
|
93966
|
-
this.deps.mintGameToken(launch.gameId, user.id, launchTokenClaims(launch))
|
|
93999
|
+
this.deps.mintGameToken(launch.gameId, user.id, launchTokenClaims(launch, partner))
|
|
93967
94000
|
]);
|
|
93968
94001
|
await this.deps.db.update(embedLaunches).set({ redeemedAt: new Date }).where(eq(embedLaunches.id, launch.id));
|
|
93969
94002
|
return {
|
|
@@ -93995,7 +94028,7 @@ class EmbedService {
|
|
|
93995
94028
|
if (outcome) {
|
|
93996
94029
|
throw new AccessDeniedError("This launch has ended");
|
|
93997
94030
|
}
|
|
93998
|
-
return this.deps.mintGameToken(launch.gameId, launch.userId, launchTokenClaims(launch));
|
|
94031
|
+
return this.deps.mintGameToken(launch.gameId, launch.userId, launchTokenClaims(launch, partner));
|
|
93999
94032
|
}
|
|
94000
94033
|
async endLaunch(launchId) {
|
|
94001
94034
|
await this.deps.db.update(embedLaunches).set({ endedAt: sql`coalesce(${embedLaunches.endedAt}, now())`, passHash: null }).where(eq(embedLaunches.id, launchId));
|
|
@@ -94015,18 +94048,17 @@ class EmbedService {
|
|
|
94015
94048
|
});
|
|
94016
94049
|
const partner = await this.deps.db.query.embedPartners.findFirst({
|
|
94017
94050
|
where: eq(embedPartners.id, launch.partnerId),
|
|
94018
|
-
columns: { enabled: true, allowedOrigins: true }
|
|
94051
|
+
columns: { enabled: true, allowedOrigins: true, handle: true }
|
|
94019
94052
|
});
|
|
94020
94053
|
const originMatches = launch.parentOrigin === input.parentOrigin;
|
|
94021
|
-
|
|
94022
|
-
if (!originMatches || !stillAllowed) {
|
|
94054
|
+
if (!originMatches || !partner || !canPartnerLaunch(partner, launch.parentOrigin)) {
|
|
94023
94055
|
await this.endLaunch(launch.id);
|
|
94024
94056
|
setAttributes({
|
|
94025
94057
|
"app.embed.outcome": originMatches ? "partner_disallowed" : "origin_mismatch"
|
|
94026
94058
|
});
|
|
94027
94059
|
throw new UnauthorizedError("This launch pass is not valid");
|
|
94028
94060
|
}
|
|
94029
|
-
return launch;
|
|
94061
|
+
return { launch, partner };
|
|
94030
94062
|
}
|
|
94031
94063
|
}
|
|
94032
94064
|
function renewalRefusal(launch, isAllowed) {
|
|
@@ -150842,7 +150874,7 @@ async function ensurePartner(ctx) {
|
|
|
150842
150874
|
await requireEmbedAdmin(ctx);
|
|
150843
150875
|
const clientId = requireClientIdParam(ctx);
|
|
150844
150876
|
const body2 = await parseRequestBody(ctx.request, EnsureEmbedPartnerSchema);
|
|
150845
|
-
return ctx.services.embedPartner.ensurePartner(clientId, body2.name);
|
|
150877
|
+
return ctx.services.embedPartner.ensurePartner(clientId, body2.name, body2.handle);
|
|
150846
150878
|
}
|
|
150847
150879
|
async function addPartnerOrigin(ctx) {
|
|
150848
150880
|
await requireEmbedAdmin(ctx);
|