@@ -229,10 +229,20 @@ let (record, q) = stack_encrypt::all((
229229 cipher . query (" bob@example.com" ). using (& email_plan ). equality (),
230230)). await ? ;
231231
232- // Typed, from the derive, which emits the same plan.
233- let record : EncryptedUser = cipher . encrypt (& user ). using (EncryptedUser :: plan ()). await ? ;
232+ // Typed, from the derive, which emits the same plan. `plan()` returns a
233+ // `Result` and is generic over the key source, so it is borrowed after `?`.
234+ let record : EncryptedUser = cipher . encrypt (& user ). using (& EncryptedUser :: plan ()? ). await ? ;
234235```
235236
237+ The one-value start has a context-field form too: ` Plan::value::<S>().context_field::<C>() `
238+ carries the call's context (a ` NonEmpty<C> ` ) out beside the output, which is
239+ ` (C, T) ` . ` encryption_with_context ` and `decryption_with_context(record,
240+ ExpectedContext<C >)` run it, and the stored context is checked against the
241+ ` ExpectedContext ` before any key request. A tuple of targets takes the context
242+ every element accepts (` JoinContext ` ): the shared one when all agree, an
243+ ` AeadContext ` for ciphertexts alone, and a ` CallerContext ` for a ciphertext
244+ beside a term.
245+
236246` keyset(..) ` may also sit on the cipher as it does today
237247(` cipher.keyset("tenant-42").encrypt(..) ` ); both forms coexist because a
238248` KeysetCipher ` already exists. The chain form is there so a saved call site
@@ -265,6 +275,15 @@ A plan takes its context from exactly one of three sources:
265275 record can be opened, and on open it is checked against the context each
266276 field was sealed under.
267277
278+ A one-value plan also has a typed call-context path,
279+ ` encryption_with_context::<K, C>() ` , which runs the plan under the caller's
280+ context exactly as given: a ` NonEmpty ` of any context type, an integer or an
281+ AEAD-only context, not only a ` Label ` . This is how a derived ` plaintext = T `
282+ record receives its caller's context. Parsing it into a label would change its
283+ bytes (` nonempty!("users/email") ` is one escaped part, ` Label::parse("users/email") `
284+ is two segments), so a two-part caller context stays one escaped part. A plan
285+ built with its own context refuses this path (` TwoContextSources ` ), without I/O.
286+
268287Two sources is an error. ` .extend(parts) ` is the only way to add to the
269288context, and it extends whichever source the plan has. A plan given two
270289sources is refused (` TwoContextSources ` ), at build or when the call adds one.
@@ -408,7 +427,7 @@ Items 1 to 4 shipped in #1068 and #1069; #1071 built the chain on them.
408427### Consolidation
409428
410429- ** The derive emits the plan, after the derive is narrowed and the plan
411- widened.** The derive and a data plan are two authors of one grammar over
430+ widened (done in # 1076 ) .** The derive and a data plan are two authors of one grammar over
412431 one executor; the derive is not kept on the combinators.
413432 ` #[derive(EncryptFrom)] ` generates ` EncryptedUser::plan() ` as a plan chain
414433 in the picker form, and ` EncryptFrom::encryption() ` returns the plan's
@@ -435,6 +454,20 @@ Items 1 to 4 shipped in #1068 and #1069; #1071 built the chain on them.
435454 typed verb ` encrypt_into ` in both positions, with ` .with(indexes) ` as its
436455 sugar; ` Encrypted<Terms>: EncryptFrom<S> ` with its byte-identity test; and
437456 the picker.
457+
458+ What #1076 settled, beyond the list above:
459+ - A ` struct ` record's ` DecryptInto ` opens each field through its own
460+ ` DecryptField ` under ` <context>/<identity> ` (extended by the caller's
461+ context), which is what the plan's opener does for a typed field, rather
462+ than through ` Plan::decryption ` . A decrypt-only derive has field types with
463+ no ` EncryptFrom ` , and a plan cannot be built without it. For the same
464+ reason a ` plaintext = T ` record's ` DecryptInto ` calls the tuple's
465+ ` DecryptInto ` directly.
466+ - Two outputs of a ` plaintext = T ` record that declare the same index are
467+ refused when run (` PlanError::DuplicateIndex ` , before any key request), not
468+ at compile time: the derive sees type names, not the indexes they declare.
469+ - No derive attribute maps to ` passthrough ` (a ` default ` field is filled with
470+ a default, not carried from the source), so the derive emits none.
438471- ** ` dynamic::record ` becomes a lowering, not an executor.** It parses the
439472 data plan and drives the builder, with ` dynamic::term ` 's dispatch from a
440473 runtime scalar to a typed index as the one step that stays dynamic. Its own
@@ -689,15 +722,15 @@ Done:
6897223 . ** The builder** (#1071 ): the one-value chain, the fields chain, ` Plan ` ,
690723 ` using ` , ` query ` , ` open ` , ` all ` .
691724
692- Next, as stacked drafts on #1071 :
725+ Next, as stacked drafts on #1071 (4, 5 and 6 are open drafts) :
693726
694- 4 . ** Narrow the derive** : the four removals in "Consolidation". The byte
695- changes are isolated in this PR and listed.
696- 5 . ** Widen the plan** : the two starts, the three context sources,
697- ` encrypt_into ` in both positions, ` Encrypted<Terms>: EncryptFrom<S> ` , the
698- picker, and the match and JSON options on the wire.
699- 6 . ** The derive emits the plan** (#1058 ), with byte identity proven on the
700- narrowed grammar.
727+ 4 . ** Narrow the derive** ( # 1073 , open draft) : the four removals in
728+ "Consolidation". The byte changes are isolated in this PR and listed.
729+ 5 . ** Widen the plan** ( # 1074 , open draft) : the two starts, the three context
730+ sources, ` encrypt_into ` in both positions, ` Encrypted<Terms>: EncryptFrom<S> ` ,
731+ the picker, and the match and JSON options on the wire.
732+ 6 . ** The derive emits the plan** (#1058 , PR # 1076 , open draft), with byte
733+ identity proven on the narrowed grammar.
701734
702735Then:
703736
0 commit comments