{"id":51683908,"url":"https://github.com/nathanosdev/ssn-test","last_synced_at":"2026-07-15T17:10:57.886Z","repository":{"id":362363776,"uuid":"1210841153","full_name":"nathanosdev/ssn-test","owner":"nathanosdev","description":null,"archived":false,"fork":false,"pushed_at":"2026-04-14T20:10:08.000Z","size":45,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-03T22:27:13.396Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nathanosdev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-14T20:09:31.000Z","updated_at":"2026-04-14T20:10:13.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/nathanosdev/ssn-test","commit_stats":null,"previous_names":["nathanosdev/ssn-test"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/nathanosdev/ssn-test","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nathanosdev%2Fssn-test","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nathanosdev%2Fssn-test/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nathanosdev%2Fssn-test/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nathanosdev%2Fssn-test/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nathanosdev","download_url":"https://codeload.github.com/nathanosdev/ssn-test/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nathanosdev%2Fssn-test/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35513661,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-15T02:00:06.706Z","response_time":131,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-07-15T17:10:55.500Z","updated_at":"2026-07-15T17:10:57.880Z","avatar_url":"https://github.com/nathanosdev.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Serving static assets over HTTP\n\nThis guide walks through an example project that demonstrates how to create a canister that can serve certified static assets (HTML, CSS, JS) over HTTP. The example project presents a very simple single page JavaScript application. Assets are embedded into the canister when it is compiled.\n\nThis is not a beginner's canister development guide. Many fundamental concepts that a relatively experienced canister developer should already know will be omitted. Concepts specific to asset certification will be called out here and can help to understand the [full code example](https://github.com/dfinity/response-verification/tree/main/examples/http-certification/assets).\n\nThe certification and serving of assets is based on the high-level [`ic-asset-certification` crate](https://crates.io/crates/ic-asset-certification).\n\nIf more flexibility than what this crate provides is needed, the lower-level [`ic-http-certification` crate](https://crates.io/crates/ic-http-certification) can be used. Be sure to check out the [\"Custom HTTP canisters\"](https://internetcomputer.org/docs/current/developer-docs/http-compatible-canisters/custom-http-canisters) and [\"Custom asset canisters\"](https://internetcomputer.org/docs/current/developer-docs/web-apps/http-compatible-canisters/serving-static-assets-over-http) guides to learn more about how to use that library for serving assets.\n\n## The frontend assets\n\nThe frontend project used for this example is a simple starter project generated with `npx degit solidjs/templates/ts my-app`. The only changes that have been made are in the `vite.config.ts` file. The `vite-plugin-compression` plugin was added and configured to generate Gzip and Brotli encoded assets, alongside the original assets. The `ext` configuration affects the file extension and it's important to keep this consistent with the backend canister code that will be seen later in this guide.\n\n```ts\nimport { defineConfig } from 'vite';\nimport solidPlugin from 'vite-plugin-solid';\n\n// import the compression plugin\nimport viteCompressionPlugin from 'vite-plugin-compression';\n\nexport default defineConfig({\n  plugins: [\n    solidPlugin(),\n\n    // setup Gzip compression\n    viteCompressionPlugin({\n      algorithm: 'gzip',\n      // this extension will be referenced later in the canister code\n      ext: '.gz',\n      // ensure to not delete the original files\n      deleteOriginFile: false,\n      threshold: 0,\n    }),\n\n    // setup Brotli compression\n    viteCompressionPlugin({\n      algorithm: 'brotliCompress',\n      // this extension will be referenced later in the canister code\n      ext: '.br',\n      // ensure to not delete the original files\n      deleteOriginFile: false,\n      threshold: 0,\n    }),\n  ],\n  server: {\n    port: 3000,\n  },\n  build: {\n    target: 'esnext',\n  },\n});\n```\n\nThe rest of this guide will address the canister code.\n\n## Lifecycle hooks\n\nThe first thing to do when the canister bootstraps for the first time is to certify all the assets. This is done in the `init` hook. The `certify_all_assets` function will be covered in a later section.\n\nThe asset certification is not stored in stable memory so it's necessary to re-certify the assets after a canister upgrade. This is done in the `post_upgrade` hook.\n\n```rust\n#[init]\nfn init() {\n    certify_all_assets();\n}\n\n#[post_upgrade]\nfn post_upgrade() {\n    init();\n}\n```\n\n## Canister endpoints\n\nThere is only one canister endpoint in this example to serve assets, the `http_request` query endpoint. The `http_request` handler uses two auxiliary functions, `serve_metrics` and `serve_asset`, which are covered in a later section.\n\n```rust\n#[query]\nfn http_request(req: HttpRequest) -\u003e HttpResponse {\n    let path = req.get_path().expect(\"Failed to parse request path\");\n\n    // if the request is for the metrics endpoint, serve the metrics\n    if path == \"/metrics\" {\n        return serve_metrics();\n    }\n\n    // otherwise, serve the requested asset\n    serve_asset(\u0026req)\n}\n```\n\n## Loading assets\n\nAssets are embedded into the canister's Wasm at build time. This is achieved using the [`include_dir`](https://michael-f-bryan.github.io/include_dir/include_dir/index.html) crate. Note that this works fine for a small number of assets, but a larger number of assets may cause longer compile times, as mentioned in the [crate's documentation](https://michael-f-bryan.github.io/include_dir/include_dir/index.html#compile-time-considerations).\n\nThe assets are imported from the frontend build directory:\n\n```rust\nstatic ASSETS_DIR: Dir\u003c'_\u003e = include_dir!(\"$CARGO_MANIFEST_DIR/../frontend/dist\");\n```\n\nWith the assets loaded, they need to be converted into the `Asset` type that the `ic-asset-certification` crate uses.\n\n```rust\n/// Rescursively collect all assets from the provided directory\nfn collect_assets\u003c'content, 'path\u003e(\n    dir: \u0026'content Dir\u003c'path\u003e,\n    assets: \u0026mut Vec\u003cAsset\u003c'content, 'path\u003e\u003e,\n) {\n    for file in dir.files() {\n        assets.push(Asset::new(file.path().to_string_lossy(), file.contents()));\n    }\n\n    for dir in dir.dirs() {\n        collect_assets(dir, assets);\n    }\n}\n```\n\n## Certifying assets\n\nAsset certification is configured using the `AssetConfig` type. This type is used to specify the content type, headers, and any fallbacks for each asset.\n\nTo handle common headers, a helper function `get_asset_headers` is used. The security headers added to responses are based on the [OWASP Secure Headers project](https://owasp.org/www-project-secure-headers/index.html).\n\nThese security headers have been included as a reasonably secure default for most static asset APIs. However, it's vitally important for developers to educate themselves and make informed decisions in the context of their own project's needs.\n\n```rust\nfn get_asset_headers(additional_headers: Vec\u003cHeaderField\u003e) -\u003e Vec\u003cHeaderField\u003e {\n    // set up the default headers and include additional headers provided by the caller\n    let mut headers = vec![\n        (\"strict-transport-security\".to_string(), \"max-age=31536000; includeSubDomains\".to_string()),\n        (\"x-frame-options\".to_string(), \"DENY\".to_string()),\n        (\"x-content-type-options\".to_string(), \"nosniff\".to_string()),\n        (\"content-security-policy\".to_string(), \"default-src 'self'; img-src 'self' data:; form-action 'self'; object-src 'none'; frame-ancestors 'none'; upgrade-insecure-requests; block-all-mixed-content\".to_string()),\n        (\"referrer-policy\".to_string(), \"no-referrer\".to_string()),\n        (\"permissions-policy\".to_string(), \"accelerometer=(),ambient-light-sensor=(),autoplay=(),battery=(),camera=(),display-capture=(),document-domain=(),encrypted-media=(),fullscreen=(),gamepad=(),geolocation=(),gyroscope=(),layout-animations=(self),legacy-image-formats=(self),magnetometer=(),microphone=(),midi=(),oversized-images=(self),payment=(),picture-in-picture=(),publickey-credentials-get=(),speaker-selection=(),sync-xhr=(self),unoptimized-images=(self),unsized-media=(self),usb=(),screen-wake-lock=(),web-share=(),xr-spatial-tracking=()\".to_string()),\n        (\"cross-origin-embedder-policy\".to_string(), \"require-corp\".to_string()),\n        (\"cross-origin-opener-policy\".to_string(), \"same-origin\".to_string()),\n    ];\n    headers.extend(additional_headers);\n\n    headers\n}\n```\n\nFor the `index.html` file, the `AssetConfig::File` variant is used to target the configuration to that file specifically. The `fallback_for` field of this variant is used to specify that this asset is the fallback for all paths that don't exactly match a file and the `aliased_by` field is used to specify alternative paths that will serve the asset.\n\nFor the remaining files, they can all be configured in bulk using the `AssetConfig::Pattern` variant. This variant uses a glob pattern to match multiple files.\n\nThe `certify_all_assets` function performs the following steps:\n\n1. Define the asset certification configurations.\n2. Collect all assets from the frontend build directory.\n3. Skip certification for the `/metrics` endpoint.\n4. Certify the assets using the `certify_assets` function from the `ic-asset-certification` crate.\n5. Set the canister's certified data.\n\n```rust\nthread_local! {\n    static HTTP_TREE: Rc\u003cRefCell\u003cHttpCertificationTree\u003e\u003e = Default::default();\n\n    static ASSET_ROUTER: RefCell\u003cAssetRouter\u003c'static\u003e\u003e = RefCell::new(AssetRouter::with_tree(HTTP_TREE.with(|tree| tree.clone())));\n\n    // initializing the asset router with an HTTP certification tree is optional.\n    // if direct access to the HTTP certification tree is not needed for certifying\n    // requests and responses outside of the asset router, then this step can be skipped\n    // and the asset router can be initialized like so:\n    static ASSET_ROUTER: RefCell\u003cAssetRouter\u003c'static\u003e\u003e = Default::default();\n}\n\nconst IMMUTABLE_ASSET_CACHE_CONTROL: \u0026str = \"public, max-age=31536000, immutable\";\nconst NO_CACHE_ASSET_CACHE_CONTROL: \u0026str = \"public, no-cache, no-store\";\n\nfn certify_all_assets() {\n    // 1. Define the asset certification configurations.\n    let encodings = vec![\n        AssetEncoding::Brotli.default(),\n        AssetEncoding::Gzip.default(),\n    ];\n\n    let asset_configs = vec![\n        AssetConfig::File {\n            path: \"index.html\".to_string(),\n            content_type: Some(\"text/html\".to_string()),\n            headers: get_asset_headers(vec![(\n                \"cache-control\".to_string(),\n                NO_CACHE_ASSET_CACHE_CONTROL.to_string(),\n            )]),\n            fallback_for: vec![AssetFallbackConfig {\n                scope: \"/\".to_string(),\n                status_code: Some(StatusCode::OK),\n            }],\n            aliased_by: vec![\"/\".to_string()],\n            encodings: encodings.clone(),\n        },\n        AssetConfig::Pattern {\n            pattern: \"**/*.js\".to_string(),\n            content_type: Some(\"text/javascript\".to_string()),\n            headers: get_asset_headers(vec![(\n                \"cache-control\".to_string(),\n                IMMUTABLE_ASSET_CACHE_CONTROL.to_string(),\n            )]),\n            encodings: encodings.clone(),\n        },\n        AssetConfig::Pattern {\n            pattern: \"**/*.css\".to_string(),\n            content_type: Some(\"text/css\".to_string()),\n            headers: get_asset_headers(vec![(\n                \"cache-control\".to_string(),\n                IMMUTABLE_ASSET_CACHE_CONTROL.to_string(),\n            )]),\n            encodings,\n        },\n        AssetConfig::Pattern {\n            pattern: \"**/*.ico\".to_string(),\n            content_type: Some(\"image/x-icon\".to_string()),\n            headers: get_asset_headers(vec![(\n                \"cache-control\".to_string(),\n                IMMUTABLE_ASSET_CACHE_CONTROL.to_string(),\n            )]),\n            encodings: vec![],\n        },\n        AssetConfig::Redirect {\n            from: \"/old-url\".to_string(),\n            to: \"/\".to_string(),\n            kind: AssetRedirectKind::Permanent,\n            headers: get_asset_headers(vec![\n                (\"content-type\".to_string(), \"text/plain\".to_string()),\n                (\n                    \"cache-control\".to_string(),\n                    NO_CACHE_ASSET_CACHE_CONTROL.to_string(),\n                ),\n            ]),\n        },\n    ];\n\n    // 2. Collect all assets from the frontend build directory.\n    let mut assets = Vec::new();\n    collect_assets(\u0026ASSETS_DIR, \u0026mut assets);\n\n    // 3. Skip certification for the metrics endpoint.\n    HTTP_TREE.with(|tree| {\n        let mut tree = tree.borrow_mut();\n\n        let metrics_tree_path = HttpCertificationPath::exact(\"/metrics\");\n        let metrics_certification = HttpCertification::skip();\n        let metrics_tree_entry =\n            HttpCertificationTreeEntry::new(metrics_tree_path, metrics_certification);\n\n        tree.insert(\u0026metrics_tree_entry);\n    });\n\n    ASSET_ROUTER.with_borrow_mut(|asset_router| {\n        // 4. Certify the assets using the `certify_assets` function from the `ic-asset-certification` crate.\n        if let Err(err) = asset_router.certify_assets(assets, asset_configs) {\n            ic_cdk::trap(\u0026format!(\"Failed to certify assets: {}\", err));\n        }\n\n        // 5. Set the canister's certified data.\n        set_certified_data(\u0026asset_router.root_hash());\n    });\n}\n```\n\n## Serving assets\n\nThe `serve_asset` function from the `AssetRouter` is responsible for serving assets. This function returns an `HttpResponse` that can be returned to the caller.\n\n```rust\nfn serve_asset(req: \u0026HttpRequest) -\u003e HttpResponse\u003c'static\u003e {\n    ASSET_ROUTER.with_borrow(|asset_router| {\n        if let Ok(response) = asset_router.serve_asset(\n            \u0026data_certificate().expect(\"No data certificate available\"),\n            req,\n        ) {\n            response\n        } else {\n            ic_cdk::trap(\"Failed to serve asset\");\n        }\n    })\n}\n```\n\n## Serving metrics\n\nThe `serve_metrics` function is responsible for serving metrics. Since metrics are not certified, this procedure is a bit more involved compared to serving assets, which is handled entirely by the `asset_router`.\n\nIt's important to determine whether skipping certification is appropriate for the use case. In this example, metrics are not sensitive data and are not used to make decisions that could affect the canister's security. Therefore, it's determined to be acceptable to skip certification for this use case, but that may not be the case for every canister. The important takeaway from this example is to learn how to skip certification, when it is necessary and safe to do so.\n\nThe `Metrics` struct is used to collect the number of assets, number of fallback assets, and the cycle balance and serialize this into JSON. The `add_v2_certificate_header` function from the `ic-http-certification` library is used to add the `IC-Certificate` header to the response and then the `IC-Certificate-Expression` header is added too. The `get_asset_headers` function is used to get the same headers for the response that are used for asset responses.\n\n```rust\nfn serve_metrics() -\u003e HttpResponse\u003c'static\u003e {\n    ASSET_ROUTER.with_borrow(|asset_router| {\n        let metrics = Metrics {\n            num_assets: asset_router.get_assets().len(),\n            num_fallback_assets: asset_router.get_fallback_assets().len(),\n            cycle_balance: canister_balance(),\n        };\n        let body = serde_json::to_vec(\u0026metrics).expect(\"Failed to serialize metrics\");\n        let headers = get_asset_headers(vec![\n            (\n                CERTIFICATE_EXPRESSION_HEADER_NAME.to_string(),\n                DefaultCelBuilder::skip_certification().to_string(),\n            ),\n            (\"content-type\".to_string(), \"application/json\".to_string()),\n            (\n                \"cache-control\".to_string(),\n                NO_CACHE_ASSET_CACHE_CONTROL.to_string(),\n            ),\n        ]);\n        let mut response = HttpResponse::builder()\n            .with_status_code(200)\n            .with_body(body)\n            .with_headers(headers)\n            .build();\n\n        HTTP_TREE.with(|tree| {\n            let tree = tree.borrow();\n\n            let metrics_tree_path = HttpCertificationPath::exact(\"/metrics\");\n            let metrics_certification = HttpCertification::skip();\n            let metrics_tree_entry =\n                HttpCertificationTreeEntry::new(\u0026metrics_tree_path, metrics_certification);\n            add_v2_certificate_header(\n                \u0026data_certificate().expect(\"No data certificate available\"),\n                \u0026mut response,\n                \u0026tree.witness(\u0026metrics_tree_entry, \"/metrics\").unwrap(),\n                \u0026metrics_tree_path.to_expr_path(),\n            );\n\n            response\n        })\n    })\n}\n```\n\n## Testing the canister\n\nThis example uses a canister called `http_certification_assets_backend`.\n\nTo test the canister, you can use [`dfx`](https://internetcomputer.org/docs/building-apps/getting-started/install) to start a local development environment:\n\n```shell\ndfx start --background --clean\n```\n\nThen, deploy the canister:\n\n```shell\ndfx deploy http_certification_assets_backend\n```\n\nYou can now access the canister's assets by navigating to the canister's URL in a web browser. The URL can also be found using the following command:\n\n```shell\necho \"http://$(dfx canister id http_certification_assets_backend).localhost:$(dfx info webserver-port)\"\n```\n\nAlternatively, to make a request with `curl`:\n\n```shell\ncurl \"http://$(dfx canister id http_certification_assets_backend).localhost:$(dfx info webserver-port)\" --resolve \"$(dfx canister id http_certification_assets_backend).localhost:$(dfx info webserver-port):127.0.0.1\"\n```\n\n## Resources\n\n- [Example source code](https://github.com/dfinity/response-verification/tree/main/examples/http-certification/assets).\n- [`ic-asset-certification` crate](https://crates.io/crates/ic-asset-certification).\n- [`ic-asset-certification` docs](https://docs.rs/ic-asset-certification/latest/ic_asset_certification).\n- [`ic-asset-certification` source code](https://github.com/dfinity/response-verification/tree/main/packages/ic-asset-certification).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnathanosdev%2Fssn-test","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnathanosdev%2Fssn-test","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnathanosdev%2Fssn-test/lists"}