feat(sites): mint a licence for the published sites
Sites / deploy (push) Successful in 4m4s
Build / Build-and-ng-test (pull_request) Successful in 5m31s
Lighthouse Checks / lighthouse (pull_request) Successful in 20m58s
Build / Build-and-test-development (pull_request) Failing after 36m15s

The demo and staging sites ran on the free tier, so uploads and the other
licensed features were unavailable to anyone looking at them. The Sites job now
mints a self-contained key pair (site_id 123, the estate's SYSSITE) into the
drive before the snapshot, so both sites are fully usable.

Minted per deploy rather than committed, and the CI estate stays unlicensed so
the specs keep exercising the free tier.
This commit is contained in:
dc committed 2026-10-24 20:32:08 +00:00
1 parent 75b4acf1b1
commit 7d9df71fdb
3 files changed
+99 -1

No files matched your search

+6
View File
@@ -113,6 +113,12 @@ jobs:
- name: Build the browser-hosted site
run: |
# The published sites ship a working licence so they are usable beyond
# the free tier. It is minted per deploy rather than committed: the
# pair is self-contained, and the CI estate stays unlicensed so the
# specs can exercise the free tier.
node sas/mocks/mint-licence.js "sasjs_root/drive/files$APP_LOC/mock-storage/licence.json"
# DC's data files exist only after makedata has run, so the drive is
# captured first and shipped as the site's starting drive. services/web9
# is the streamed frontend, which is already the site itself.
+1 -1
View File
@@ -135,7 +135,7 @@ After changing mock services, re-run `sasjs cbd -t server-ci` from `sas/mocks/sa
### Demo and staging sites
A push to `release/**` publishes [staging.datacontroller.io](https://staging.datacontroller.io); a release tag (`v*`, created by the release job after its tests pass) publishes [demo.datacontroller.io](https://demo.datacontroller.io) - workflow file `.gitea/workflows/sites.yaml`, using the `DEMO_DATACONTROLLER_IO` Surfer app password (user `allan`). Both are sites on one multi-site Surfer instance; the demo is its default site. The job builds the mocks as a browser-hosted site (SASjs CLI >= 6.1, `streamConfig.mockWeb`), so the services run in a service worker and the drive lives in the visitor's IndexedDB - there is no server. It runs `makedata` on a throwaway SASjs Server first and ships that drive as `sasjsbuild/mock-snapshot.json`; without it the site opens with no data. The estate carries no licence key, so it runs on the free tier until a key is registered through the licensing screen (`admin/registerkey`); the Cypress setup does that for the specs that need one.
A push to `release/**` publishes [staging.datacontroller.io](https://staging.datacontroller.io); a release tag (`v*`, created by the release job after its tests pass) publishes [demo.datacontroller.io](https://demo.datacontroller.io) - workflow file `.gitea/workflows/sites.yaml`, using the `DEMO_DATACONTROLLER_IO` Surfer app password (user `allan`). Both are sites on one multi-site Surfer instance; the demo is its default site. The job builds the mocks as a browser-hosted site (SASjs CLI >= 6.1, `streamConfig.mockWeb`), so the services run in a service worker and the drive lives in the visitor's IndexedDB - there is no server. It runs `makedata` on a throwaway SASjs Server first and ships that drive as `sasjsbuild/mock-snapshot.json`; without it the site opens with no data. The estate carries no licence key, so CI runs on the free tier until a key is registered through the licensing screen (`admin/registerkey`) - the Cypress setup does that for the specs that need one. The Sites job mints a licence for the published sites, so demo and staging are fully usable.
## GUI Elements
+92
View File
@@ -0,0 +1,92 @@
// Mints a Data Controller licence key pair for a published mock site.
//
// The app decrypts `licenceKey` with `activationKey`:
// activationKey = base64(pkcs8 RSA-OAEP private key)
// licenceKey = base64(RSA-OAEP-SHA256(JSON licence data, public key))
//
// The two values must differ: that is how the app tells an HTTPS-format key
// from an HTTP-format one (see detectLicenceKeyProtocolMismatch.ts). The
// payload's site_id must match the estate's SYSSITE, which the mock reports
// as 123.
//
// The Sites workflow runs this against the drive before the snapshot is taken,
// so demo.datacontroller.io and staging.datacontroller.io are licensed. The
// pair is minted per deploy rather than committed: it is self-contained, and
// the CI estate stays unlicensed so the specs can exercise the free tier.
//
// Usage: node sas/mocks/mint-licence.js <out-file>
const fs = require('fs')
const SITE_ID = '123' // the mock estate's SYSSITE
const OUT = process.argv[2]
const b64 = (buf) => Buffer.from(buf).toString('base64')
async function main() {
if (!OUT) {
console.error('Usage: node sas/mocks/mint-licence.js <out-file>')
process.exit(1)
}
const keyPair = await crypto.subtle.generateKey(
{
name: 'RSA-OAEP',
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]),
hash: 'SHA-256'
},
true,
['encrypt', 'decrypt']
)
const licenceData = {
valid_until: '2099-12-31',
users_allowed: 9999,
hot_license_key: 'non-commercial-and-evaluation',
site_id: SITE_ID,
demo: false
}
const plaintext = new TextEncoder().encode(JSON.stringify(licenceData))
const cipher = await crypto.subtle.encrypt(
{ name: 'RSA-OAEP' },
keyPair.publicKey,
plaintext
)
const privateKeyBytes = await crypto.subtle.exportKey(
'pkcs8',
keyPair.privateKey
)
const licenceKey = b64(cipher)
const activationKey = b64(privateKeyBytes)
// Round-trip check with the same WebCrypto the app uses, so a minted pair is
// known to decrypt before it is published.
const imported = await crypto.subtle.importKey(
'pkcs8',
privateKeyBytes,
{ name: 'RSA-OAEP', hash: 'SHA-256' },
true,
['decrypt']
)
const back = await crypto.subtle.decrypt(
{ name: 'RSA-OAEP', hash: 'SHA-256' },
imported,
cipher
)
const decoded = JSON.parse(new TextDecoder().decode(back))
if (JSON.stringify(decoded) !== JSON.stringify(licenceData))
throw new Error('round trip mismatch')
if (licenceKey === activationKey)
throw new Error('keys must differ to be an HTTPS-format key')
fs.writeFileSync(OUT, JSON.stringify({ licenceKey, activationKey }, null, 2))
console.log(`minted licence for site ${SITE_ID} -> ${OUT}`)
}
main().catch((err) => {
console.error('FAILED', err)
process.exit(1)
})