Skip to content

Commit 303efce

Browse files
authored
Add browser contained site resolution DTO (#1226)
1 parent f07be91 commit 303efce

6 files changed

Lines changed: 117 additions & 14 deletions

packages/wordpress-plugin/src/class-wp-codebox-browser-ability-descriptors.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ public static function descriptors( array $context ): array {
142142
'site_id' => array( 'type' => 'string' ),
143143
'source_digest' => array( 'type' => 'object' ),
144144
'status' => array( 'type' => 'string' ),
145+
'resolution' => array( 'type' => 'object' ),
145146
'prepared_runtime' => array( 'type' => 'object' ),
146147
'blueprint_ref' => array( 'type' => 'object' ),
147148
),
@@ -176,6 +177,7 @@ public static function descriptors( array $context ): array {
176177
'schema' => array( 'type' => 'string', 'const' => 'wp-codebox/browser-contained-site-open/v1' ),
177178
'site_id' => array( 'type' => 'string' ),
178179
'status' => array( 'type' => 'string' ),
180+
'resolution' => array( 'type' => 'object' ),
179181
'contained_site' => $context['browser_contained_site_schema'],
180182
'source_digest' => array( 'type' => 'object' ),
181183
'prepared_runtime' => array( 'type' => 'object' ),

packages/wordpress-plugin/src/class-wp-codebox-browser-task-builder.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,7 @@ public static function browser_blueprint_ref( array $prepared, array $session =
437437
);
438438
}
439439

440-
/** @param array<string,mixed> $input Prepared runtime, full session, or Studio executable ref request. @return array<string,mixed> */
440+
/** @param array<string,mixed> $input Prepared runtime, full session, or executable ref request. @return array<string,mixed> */
441441
public static function executable_blueprint_ref( array $input ): array {
442442
$session = is_array( $input['session'] ?? null ) ? $input['session'] : $input;
443443
$primary = is_array( $session['primary'] ?? null ) ? $session['primary'] : $session;

packages/wordpress-plugin/src/trait-wp-codebox-abilities-execution.php

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ public static function open_browser_contained_site( array $input ): array|WP_Err
184184
$session_id = (string) ( $session['session']['id'] ?? '' );
185185
$preview_id = (string) ( $contained_site['preview_id'] ?? $input['preview_id'] ?? '' );
186186
$scope = (string) ( $preview_boot['scope'] ?? $contained_site['preview']['scope'] ?? '' );
187+
$resolution = is_array( $status['resolution'] ?? null ) ? $status['resolution'] : array();
187188

188189
$opened_site = array_filter(
189190
array_merge(
@@ -194,6 +195,7 @@ public static function open_browser_contained_site( array $input ): array|WP_Err
194195
'preview_id' => $preview_id,
195196
'session_id' => $session_id,
196197
'status' => (string) ( $status['status'] ?? 'miss' ),
198+
'resolution' => $resolution,
197199
'persistence' => 'browser-contained',
198200
'source_digest' => is_array( $status['source_digest'] ?? null ) ? $status['source_digest'] : array(),
199201
'prepared_runtime' => is_array( $status['prepared_runtime'] ?? null ) ? $status['prepared_runtime'] : array(),
@@ -213,6 +215,7 @@ public static function open_browser_contained_site( array $input ): array|WP_Err
213215
'schema' => 'wp-codebox/browser-contained-site-open/v1',
214216
'site_id' => $site_id,
215217
'status' => (string) ( $status['status'] ?? 'miss' ),
218+
'resolution' => $resolution,
216219
'contained_site' => $opened_site,
217220
'source_digest' => is_array( $status['source_digest'] ?? null ) ? $status['source_digest'] : array(),
218221
'prepared_runtime' => is_array( $status['prepared_runtime'] ?? null ) ? $status['prepared_runtime'] : array(),
@@ -982,6 +985,7 @@ private static function blocked_browser_playground_session( string $session_id,
982985
private static function browser_contained_site_envelope( array $input, string $session_id, array $playground, array $runtime, array $prepared_runtime, string $status ): array {
983986
$source_digest = self::browser_contained_site_source_digest( $input, $playground, $runtime, $prepared_runtime );
984987
$caller_id = self::browser_contained_site_caller_id( $input );
988+
$artifact_meta = self::browser_contained_site_artifact_meta( $input );
985989
$cache_key = self::safe_key( (string) ( $prepared_runtime['cache_key'] ?? '' ) );
986990
if ( '' === $cache_key ) {
987991
$cache_key = 'site-' . substr( hash( 'sha256', $caller_id . ':' . $source_digest ), 0, 16 );
@@ -998,6 +1002,8 @@ private static function browser_contained_site_envelope( array $input, string $s
9981002
'caller_id' => $caller_id,
9991003
'status' => $status,
10001004
'persistence' => 'browser-contained',
1005+
'artifact_seed' => (string) ( $artifact_meta['seed'] ?? '' ),
1006+
'artifact_revision' => (string) ( $artifact_meta['revision'] ?? '' ),
10011007
'recovery' => array(
10021008
'ability' => 'wp-codebox/get-browser-contained-site-status',
10031009
'input' => array(
@@ -1032,17 +1038,32 @@ private static function browser_contained_site_envelope( array $input, string $s
10321038
);
10331039
}
10341040

1041+
/** @return array{seed?:string,revision?:string} */
1042+
private static function browser_contained_site_artifact_meta( array $input ): array {
1043+
$artifact = is_array( $input['site_blueprint_artifact'] ?? null ) ? $input['site_blueprint_artifact'] : array();
1044+
1045+
return array_filter(
1046+
array(
1047+
'seed' => (string) ( $input['artifact_seed'] ?? $artifact['seed'] ?? $artifact['id'] ?? $artifact['ref'] ?? '' ),
1048+
'revision' => (string) ( $input['artifact_revision'] ?? $input['revision'] ?? $artifact['revision'] ?? $artifact['version'] ?? '' ),
1049+
),
1050+
static fn( string $value ): bool => '' !== $value
1051+
);
1052+
}
1053+
10351054
/** @return array<string,mixed> */
10361055
private static function browser_contained_site_status_envelope( string $cache_key, string $input_hash, array $lookup ): array {
10371056
$artifact = is_array( $lookup['artifact'] ?? null ) ? $lookup['artifact'] : array();
1038-
$status = 'hit' === (string) ( $lookup['status'] ?? '' ) ? 'recoverable' : (string) ( $lookup['status'] ?? 'miss' );
1057+
$status = self::browser_contained_site_status_from_lookup( $lookup );
1058+
$resolution = self::browser_contained_site_resolution( $status, $lookup );
10391059

10401060
return array_filter(
10411061
array(
10421062
'success' => 'recoverable' === $status,
10431063
'schema' => 'wp-codebox/browser-contained-site-status/v1',
10441064
'site_id' => $cache_key,
10451065
'status' => $status,
1066+
'resolution' => $resolution,
10461067
'source_digest' => array(
10471068
'algorithm' => 'sha256',
10481069
'value' => $input_hash,
@@ -1052,6 +1073,7 @@ private static function browser_contained_site_status_envelope( string $cache_ke
10521073
'cache_key' => $cache_key,
10531074
'input_hash' => $input_hash,
10541075
'status' => (string) ( $lookup['status'] ?? '' ),
1076+
'reason' => (string) ( $resolution['reason'] ?? '' ),
10551077
'created_at' => (string) ( $artifact['created_at'] ?? '' ),
10561078
),
10571079
static fn( string $value ): bool => '' !== $value
@@ -1062,6 +1084,48 @@ private static function browser_contained_site_status_envelope( string $cache_ke
10621084
);
10631085
}
10641086

1087+
/** @return string */
1088+
private static function browser_contained_site_status_from_lookup( array $lookup ): string {
1089+
$lookup_status = (string) ( $lookup['status'] ?? 'miss' );
1090+
if ( 'hit' === $lookup_status ) {
1091+
return 'recoverable';
1092+
}
1093+
1094+
if ( ! empty( $lookup['invalidation'] ) ) {
1095+
return 'incompatible';
1096+
}
1097+
1098+
return '' !== $lookup_status ? $lookup_status : 'miss';
1099+
}
1100+
1101+
/** @return array<string,mixed> */
1102+
private static function browser_contained_site_resolution( string $status, array $lookup ): array {
1103+
$invalidation = is_array( $lookup['invalidation'] ?? null ) ? $lookup['invalidation'] : array();
1104+
$reason = (string) ( $invalidation['reason'] ?? '' );
1105+
if ( '' === $reason ) {
1106+
$reason = match ( $status ) {
1107+
'recoverable' => 'prepared-runtime-cache-hit',
1108+
'incompatible' => 'prepared-runtime-incompatible',
1109+
'disabled' => 'prepared-runtime-cache-disabled',
1110+
default => 'prepared-runtime-not-found-or-expired',
1111+
};
1112+
}
1113+
1114+
return array_filter(
1115+
array(
1116+
'schema' => 'wp-codebox/browser-contained-site-resolution/v1',
1117+
'outcome' => $status,
1118+
'reason' => $reason,
1119+
'reused' => 'recoverable' === $status,
1120+
'created' => false,
1121+
'expired' => 'miss' === $status ? null : false,
1122+
'miss' => 'miss' === $status,
1123+
'incompatible' => 'incompatible' === $status,
1124+
),
1125+
static fn( mixed $value ): bool => null !== $value && array() !== $value && '' !== $value
1126+
);
1127+
}
1128+
10651129
/** @return array<string,mixed> */
10661130
private static function browser_contained_site_open_session( array $input, array $contained_site, array $status ): array {
10671131
$source_digest = (string) ( $status['source_digest']['value'] ?? '' );
@@ -1124,8 +1188,12 @@ private static function browser_contained_site_public_input( array $contained_si
11241188
'persistence' => true,
11251189
'recovery' => true,
11261190
'source_digest' => true,
1191+
'artifact_seed' => true,
1192+
'artifact_revision' => true,
11271193
'preview' => true,
11281194
'prepared_runtime' => true,
1195+
'resolution' => true,
1196+
'blueprint_ref' => true,
11291197
)
11301198
);
11311199
}

packages/wordpress-plugin/src/trait-wp-codebox-abilities-permissions.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,8 @@ private static function trusted_orchestrator_authorization( array $input, string
122122
* Filters trusted browser-session callers.
123123
*
124124
* Return either a map of caller ids to scopes, or a list of grant arrays:
125-
* [ 'studio-web' => [ 'browser-session:create' ] ]
126-
* [ [ 'caller' => 'studio-web', 'scopes' => [ 'browser-session:create' ] ] ]
125+
* [ 'browser-client' => [ 'browser-session:create' ] ]
126+
* [ [ 'caller' => 'browser-client', 'scopes' => [ 'browser-session:create' ] ] ]
127127
*
128128
* @param array<int|string,mixed> $trusted_callers Trusted caller grants.
129129
* @param array<string,mixed> $authorization Explicit caller authorization payload.

packages/wordpress-plugin/src/trait-wp-codebox-abilities-schemas.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -439,15 +439,18 @@ private static function browser_product_dto_schema(): array {
439439
private static function browser_contained_site_schema(): array {
440440
return array(
441441
'type' => 'object',
442-
'description' => 'Durable browser-contained WordPress site handle. The parent product stores this envelope and can call the status ability to recover a prepared runtime blueprint when the transient still exists.',
442+
'description' => 'Durable browser-contained WordPress site handle. The caller stores this envelope and can call the status ability to recover a prepared runtime blueprint when the transient still exists.',
443443
'properties' => array(
444444
'schema' => array( 'type' => 'string', 'const' => 'wp-codebox/browser-contained-site/v1' ),
445445
'site_id' => array( 'type' => 'string' ),
446446
'preview_id' => array( 'type' => 'string' ),
447447
'session_id' => array( 'type' => 'string' ),
448448
'caller_id' => array( 'type' => 'string' ),
449-
'status' => array( 'type' => 'string', 'enum' => array( 'ready', 'blocked', 'recoverable', 'miss', 'disabled' ) ),
449+
'status' => array( 'type' => 'string', 'enum' => array( 'ready', 'blocked', 'recoverable', 'miss', 'disabled', 'incompatible' ) ),
450+
'resolution' => array( 'type' => 'object' ),
450451
'persistence' => array( 'type' => 'string', 'enum' => array( 'browser-contained' ) ),
452+
'artifact_seed' => array( 'type' => 'string' ),
453+
'artifact_revision' => array( 'type' => 'string' ),
451454
'recovery' => array( 'type' => 'object' ),
452455
'source_digest' => array( 'type' => 'object' ),
453456
'preview' => array( 'type' => 'object' ),
@@ -482,7 +485,7 @@ private static function trusted_orchestrator_authorization_schema( string $scope
482485
),
483486
'caller' => array(
484487
'type' => 'string',
485-
'description' => 'Stable caller id, for example studio-web.',
488+
'description' => 'Stable caller id, for example browser-client.',
486489
),
487490
'scope' => array(
488491
'type' => 'string',

tests/browser-contained-site-status.test.ts

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class WP_Codebox_Test_Browser_Contained_Site_Abilities {
2626
use WP_Codebox_Abilities_Execution;
2727
}
2828
29-
$cache_key = 'studio-proof';
29+
$cache_key = 'browser-site-proof';
3030
$input_hash = str_repeat( 'c', 64 );
3131
$transient_key = 'wp_codebox_browser_prepared_runtime_' . substr( hash( 'sha256', $cache_key . ':' . $input_hash ), 0, 24 );
3232
$GLOBALS['wp_codebox_test_transients'][ $transient_key ] = array(
@@ -36,6 +36,14 @@ $GLOBALS['wp_codebox_test_transients'][ $transient_key ] = array(
3636
'created_at' => '2026-06-18T00:00:00+00:00',
3737
'blueprint' => array( 'steps' => array( array( 'step' => 'login' ) ) ),
3838
);
39+
$bad_hash = str_repeat( 'e', 64 );
40+
$bad_transient_key = 'wp_codebox_browser_prepared_runtime_' . substr( hash( 'sha256', $cache_key . ':' . $bad_hash ), 0, 24 );
41+
$GLOBALS['wp_codebox_test_transients'][ $bad_transient_key ] = array(
42+
'schema' => 'wp-codebox/browser-prepared-runtime-artifact/v1',
43+
'cache_key' => $cache_key,
44+
'input_hash' => $bad_hash,
45+
'created_at' => '2026-06-18T00:00:00+00:00',
46+
);
3947
4048
$hit = WP_Codebox_Test_Browser_Contained_Site_Abilities::get_browser_contained_site_status( array(
4149
'contained_site' => array(
@@ -48,12 +56,18 @@ $miss = WP_Codebox_Test_Browser_Contained_Site_Abilities::get_browser_contained_
4856
'site_id' => $cache_key,
4957
'input_hash' => str_repeat( 'd', 64 ),
5058
) );
59+
$incompatible = WP_Codebox_Test_Browser_Contained_Site_Abilities::get_browser_contained_site_status( array(
60+
'site_id' => $cache_key,
61+
'input_hash' => $bad_hash,
62+
) );
5163
$open_hit = WP_Codebox_Test_Browser_Contained_Site_Abilities::open_browser_contained_site( array(
5264
'contained_site' => array(
5365
'schema' => 'wp-codebox/browser-contained-site/v1',
5466
'site_id' => $cache_key,
5567
'preview_id' => 'preview-proof',
5668
'session_id' => 'session-proof',
69+
'artifact_seed' => 'seed-proof',
70+
'artifact_revision' => 'revision-proof',
5771
'source_digest' => array( 'algorithm' => 'sha256', 'value' => $input_hash ),
5872
'preview' => array(
5973
'preview_public_url' => 'https://preview.example.test',
@@ -65,34 +79,49 @@ $open_hit = WP_Codebox_Test_Browser_Contained_Site_Abilities::open_browser_conta
6579
'recovery' => array( 'input' => array( 'cache_key' => $cache_key, 'input_hash' => $input_hash ) ),
6680
),
6781
'preview_lease' => array( 'status' => 'active', 'expires_at' => '2099-01-01T00:00:00+00:00' ),
68-
'runtime_profile' => array( 'id' => 'studio-native-preview', 'env' => array( 'SECRET' => 'must-not-leak' ) ),
82+
'runtime_profile' => array( 'id' => 'browser-preview', 'env' => array( 'SECRET' => 'must-not-leak' ) ),
6983
) );
7084
$open_miss = WP_Codebox_Test_Browser_Contained_Site_Abilities::open_browser_contained_site( array(
7185
'site_id' => $cache_key,
7286
'input_hash' => str_repeat( 'd', 64 ),
7387
) );
7488
75-
echo json_encode( array( 'hit' => $hit, 'miss' => $miss, 'open_hit' => $open_hit, 'open_miss' => $open_miss ), JSON_UNESCAPED_SLASHES );
89+
echo json_encode( array( 'hit' => $hit, 'miss' => $miss, 'incompatible' => $incompatible, 'open_hit' => $open_hit, 'open_miss' => $open_miss ), JSON_UNESCAPED_SLASHES );
7690
`)
7791

7892
assert.equal(result.hit.schema, "wp-codebox/browser-contained-site-status/v1")
7993
assert.equal(result.hit.success, true)
80-
assert.equal(result.hit.site_id, "studio-proof")
94+
assert.equal(result.hit.site_id, "browser-site-proof")
8195
assert.equal(result.hit.status, "recoverable")
96+
assert.equal(result.hit.resolution.outcome, "recoverable")
97+
assert.equal(result.hit.resolution.reused, true)
98+
assert.equal(result.hit.resolution.created, false)
99+
assert.equal(result.hit.resolution.miss, false)
82100
assert.equal(result.hit.source_digest.value, "c".repeat(64))
83-
assert.equal(result.hit.blueprint_ref.ref, `prepared:studio-proof:${"c".repeat(64)}`)
101+
assert.equal(result.hit.blueprint_ref.ref, `prepared:browser-site-proof:${"c".repeat(64)}`)
84102
assert.equal(result.miss.success, false)
85103
assert.equal(result.miss.status, "miss")
104+
assert.equal(result.miss.resolution.outcome, "miss")
105+
assert.equal(result.miss.resolution.miss, true)
106+
assert.equal(result.miss.resolution.reason, "prepared-runtime-not-found-or-expired")
107+
assert.equal(result.incompatible.success, false)
108+
assert.equal(result.incompatible.status, "incompatible")
109+
assert.equal(result.incompatible.resolution.incompatible, true)
110+
assert.equal(result.incompatible.resolution.reason, "source-digest-mismatch")
86111
assert.equal(result.open_hit.schema, "wp-codebox/browser-contained-site-open/v1")
87112
assert.equal(result.open_hit.success, true)
88113
assert.equal(result.open_hit.status, "recoverable")
114+
assert.equal(result.open_hit.resolution.reused, true)
89115
assert.equal(result.open_hit.contained_site.schema, "wp-codebox/browser-contained-site/v1")
90116
assert.equal(result.open_hit.contained_site.status, "recoverable")
91-
assert.equal(result.open_hit.blueprint_ref.ref, `prepared:studio-proof:${"c".repeat(64)}`)
117+
assert.equal(result.open_hit.contained_site.resolution.outcome, "recoverable")
118+
assert.equal(result.open_hit.contained_site.artifact_seed, "seed-proof")
119+
assert.equal(result.open_hit.contained_site.artifact_revision, "revision-proof")
120+
assert.equal(result.open_hit.blueprint_ref.ref, `prepared:browser-site-proof:${"c".repeat(64)}`)
92121
assert.equal(result.open_hit.blueprint_ref.hydrator_ability, "wp-codebox/hydrate-browser-blueprint-ref")
93122
assert.equal(result.open_hit.blueprint_ref.hydration_endpoint.includes("/wp-codebox/v1/browser-blueprint-ref"), true)
94123
assert.equal(result.open_hit.preview_boot.schema, "wp-codebox/browser-preview-boot-config/v1")
95-
assert.equal(result.open_hit.preview_boot.blueprint_ref, `prepared:studio-proof:${"c".repeat(64)}`)
124+
assert.equal(result.open_hit.preview_boot.blueprint_ref, `prepared:browser-site-proof:${"c".repeat(64)}`)
96125
assert.equal(result.open_hit.preview_boot.preview.preview_public_url, "https://preview.example.test")
97126
assert.equal(result.open_hit.preview_lease.schema, "wp-codebox/preview-lease/v1")
98127
assert.equal(result.open_hit.preview_lease.lease.status, "active")
@@ -103,6 +132,7 @@ assert.equal(JSON.stringify(result.open_hit).includes('"blueprint"'), false)
103132
assert.equal(JSON.stringify(result.open_hit).includes("must-not-leak"), false)
104133
assert.equal(result.open_miss.success, false)
105134
assert.equal(result.open_miss.status, "miss")
135+
assert.equal(result.open_miss.resolution.miss, true)
106136
assert.equal(result.open_miss.blueprint_ref, undefined)
107137

108138
console.log("browser contained site status ok")

0 commit comments

Comments
 (0)