Profiles
Serve a profile from a canister
Put the profile in the canister's Wasm, where any client can read it with the interface in one certified request.
ic-wasm canister.wasm -o canister.wasm metadata candid:service -f tokens.did -v public
ic-wasm canister.wasm -o canister.wasm metadata candid:profile -f tokens.profile.json -v public
ic-wasm canister.wasm metadataicp:public candid:service
icp:public candid:profileThat is the whole change: no code in the canister, two custom sections in its built Wasm module. The first holds the interface, the second the profile, and both are public. Install or upgrade the canister with this module, and anyone who knows its id can read both in one certified request. The files are the example of Write a profile.
Add the profile to the module#
ic-wasm is DFINITY's Wasm tool for canisters; install it with
cargo install ic-wasm --version 0.11.1 --locked. The commands above were run with
that version, on a module that held nothing else. metadata <name> -f <file>
writes the file's bytes into the custom section icp:<visibility> <name>;
metadata with no name lists the sections; metadata candid:profile prints
one back. ic-wasm canister.wasm info reported the profile section as 1543 bytes, the
size of tokens.profile.json.
ic-wasm's visibility defaults to private (its metadata --help
says so). A private section is readable by the canister's controllers only, so a consumer would
find nothing. fact Observed on PocketIC 16.0.0, recorded as the answer to OQ-1 in
Open questions: a private section is refused to a caller that is
not a controller.
Add candid:service too if your build has not, since the profile binds to it. The
section is served from the next install or upgrade. A profile served this
way changes with the module only: editing it changes the module hash and needs an upgrade.
Spec §8.1 also names candid-profile-kit embed canister.wasm --profile my.profile.json
for this step. That tool is not in this repository and is not published, so this page uses
ic-wasm.
Where the profile goes#
The profile document, as UTF-8 JSON without a byte order mark, is the content of the public custom
section icp:public candid:profile. @meantjs/profile exports the names as
SERVED_SECTION ("icp:public candid:profile") and
SERVED_METADATA ("candid:profile").
-
fact The content of a custom section
icp:public <name>is at/canister/<canister_id>/metadata/<name>in the state tree, and "otherwise, this path does not exist". The specification recommendsicp:public candid:servicefor the canister's Candid interface; the profile is its neighbour. Source: IC interface specification, canister information. - fact Anyone may request that path for a public section, also when the canister has no such section, and the reply is a certificate of the subnet's state, unlike a query's reply, which one replica signs. Source: IC interface specification, HTTPS interface, read state.
-
fact A canister may export at most 16 custom sections named
icp:…and 1 MiB of exported custom sections. Source: resource limits. The 1 MiB is for all of them together,candid:serviceincluded, so a served profile is at most 1 MiB (SERVED_MAX_BYTES, 1048576).
What may be served#
A profile of kind: "canister" and origin: "author". Its
binds.canister_id may be left out, so that one module can be installed as many
canisters; when it is given, it has to be the id of the canister that serves it. Bytes that are not
such a document are EE_SERVED, and the profile is not applied; the rest of the service
resolves without it.
| The served bytes | The EE_SERVED message |
|---|---|
| More than 1 MiB | the served profile is … bytes, over the 1048576 a canister can serve |
| Not UTF-8 | the served profile is not UTF-8 |
| A byte order mark first | the served profile starts with a byte order mark |
| Not JSON, or not one JSON object | the served profile is not JSON: …, … is not a JSON object |
| A pack | a canister serves a profile of kind "canister", not "pack" |
| Another origin | a profile its canister serves is the author's: origin "author", not … |
| Bound to another canister | the served profile is bound to canister "be2us-…-qaabq-cai", but bkyz2-…-qaaaq-cai served it |
How a consumer reads it#
-
One read_state for both paths#
Read
/canister/<id>/metadata/candid:serviceand/canister/<id>/metadata/candid:profilein oneread_staterequest, and verify the certificate. One certificate gives both from the same state, so the interface and the profile cannot come from two versions of the module. -
Absent is not unknown#
A path the certificate shows absent means the canister serves nothing there, and no profile is not an error. A path the certificate neither holds nor shows absent is an error: the spec says the consumer "MUST NOT read it as absent".
-
Bind it to the served interface#
Bind the profile as any other, against the interface the same canister serves as
candid:service: the sameinterface_idapplies it, a different one makes itreboundorstale(Profiles, layers and binding). A canister that serves nocandid:servicegives nothing to bind against, and the consumer binds against the interface it has from elsewhere. -
Apply it in the author layer#
A served profile is the author's. It merges above any third-party profile and any pack, so a pack tag it overrides is reported with WW_OVERRIDDEN.
Read it with @meantjs/profile#
import { createSchemaValidator, readServedProfile, resolveService } from "@meantjs/profile";
/**
* Both metadata paths come from ONE verified read_state certificate. Pass null for a path the certificate
* shows absent; a path it neither holds nor shows absent is an error, so do not call this with it.
* @param {string} canisterId
* @param {Uint8Array | null} serviceBytes /canister/<id>/metadata/candid:service
* @param {Uint8Array | null} profileBytes /canister/<id>/metadata/candid:profile
* @param {string} [fallbackDid] the interface from elsewhere, when no candid:service is served
*/
export async function resolveServed(canisterId, serviceBytes, profileBytes, fallbackDid) {
const did = serviceBytes !== null ? new TextDecoder().decode(serviceBytes) : fallbackDid;
if (did === undefined) throw new Error("no interface to bind the profile against");
const served = profileBytes === null ? null : readServedProfile(profileBytes, { canisterId });
const validateSchema = await createSchemaValidator();
const r = await resolveService({
did,
validateSchema,
profiles: served !== null && served.doc !== null ? [{ doc: served.doc, label: "candid:profile" }] : [],
});
return { ...r, ok: r.ok && (served === null || served.ok), diagnostics: [...(served?.diagnostics ?? []), ...r.diagnostics] };
}
@meantjs/profile does not fetch anything and has no agent dependency: reading the
certificate is your agent's job, and the package checks what came back.
readServedProfile(bytes, { canisterId }) returns { ok, doc, diagnostics },
with doc null and one EE_SERVED when the bytes cannot be used.
The profile it returns is not checked yet: resolveService checks its schema, its binding
and its tags as for any profile. To try the function without a canister, feed it the two files:
import { readFileSync } from "node:fs";
const service = new TextEncoder().encode(readFileSync("tokens.did", "utf8"));
const profile = new TextEncoder().encode(readFileSync("tokens.profile.json", "utf8"));
const r = await resolveServed("bkyz2-fmaaa-aaaaa-qaaaq-cai", service, profile);
console.log(r.ok, r.profiles.map((p) => `${p.status} ${p.name} ${p.origin}`), r.diagnostics);true [ 'applied tokens-example author' ] []
With a copy of the profile whose binds.canister_id names another canister, the same call
gives false, no profile, and the EE_SERVED of the table above, followed by
the two II_UNTAGGED_NUMERIC infos of a service that now has no profile.
canisterId is a string, or null to check a module that is not installed
yet, since a module does not know the id it will run as; null skips only the
canister_id check, and any other value that is not a string throws. Checking the module
before you install it works the same way: ic-wasm canister.wasm metadata candid:profile
prints the section, and readServedProfile with canisterId: null followed
by resolveService gave the example profile applied, with no diagnostic.
A tagged .did#
The candid:service a canister serves may carry doc annotations
(Annotate a .did file). They are an author profile too, read from
the same certificate: profileFromDocAttributes(await didToDocAttributes(serviceText))
turns them into one. A consumer that reads both applies both in the author layer, where two
different values for one tag at one position are EE_CONFLICT. So a canister can serve
its whole profile in the interface's comments and no candid:profile at all, or both,
as long as they agree.
Other channels#
A profile read through a query method, or fetched from any host but the canister's certified state, is not certified. A consumer may still apply it in the author layer when it trusts that channel, as for a profile the author ships with their code. A query method is not proposed as a way to serve a profile, because its reply is not certified.
What serving proves#
A served profile is certified, not vouched for. The certificate proves that the canister serves these bytes, so no replica or boundary node can change them on the way. The words are still the author's claim: a canister can serve a profile that calls a drain "claim reward", and the controller that installs the module chooses what it says. A consumer should still show the decoded arguments next to the intent for a transfer, approve, admin or destroy. Security model covers what a profile can and cannot vouch for, and Internet Computer the platform pieces this page relies on.