diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..35251f6 --- /dev/null +++ b/bun.lock @@ -0,0 +1,283 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "ng-eventually", + "devDependencies": { + "@types/bun": "latest", + "playwright": "^1.61.1", + "typescript": "^5.6.0", + }, + }, + "examples/notebook": { + "name": "@ng-eventually/example-notebook", + "dependencies": { + "@ng-eventually/client": "workspace:*", + "@ng-org/web": "0.1.2-alpha.13", + }, + }, + "packages/client": { + "name": "@ng-eventually/client", + "version": "0.0.0", + "devDependencies": { + "@ng-org/alien-deepsignals": "0.1.2-alpha.11", + "@ng-org/shex-orm": "0.1.2-alpha.8", + "@ng-org/web": "0.1.2-alpha.13", + }, + "peerDependencies": { + "@ng-org/alien-deepsignals": "*", + "@ng-org/orm": "*", + "@ng-org/shex-orm": "*", + "@ng-org/web": "*", + }, + "optionalPeers": [ + "@ng-org/alien-deepsignals", + "@ng-org/orm", + "@ng-org/shex-orm", + "@ng-org/web", + ], + }, + }, + "packages": { + "@ldo/traverser-shexj": ["@ldo/traverser-shexj@1.0.0-alpha.28", "", { "dependencies": { "@ldo/type-traverser": "1.0.0-alpha.28" } }, "sha512-N06+LOWhv6//unPRLbFMd56MqPf5lO2ihZgle9hNLmxt6QJmNrZM3oXzHCL3TfDu4OT1/NUZp3kj2HmztQIZkg=="], + + "@ldo/type-traverser": ["@ldo/type-traverser@1.0.0-alpha.28", "", { "dependencies": { "uuid": "8.3.2" } }, "sha512-pGMIVxLzoLjYVhADuVhg6r5ZDNleXZ9DcyIvLXo1/ADEocLnysg/Xjk9D/7l/Rw3WtDJrTFOOtBv8OnH+VPgKA=="], + + "@ng-eventually/client": ["@ng-eventually/client@workspace:packages/client"], + + "@ng-eventually/example-notebook": ["@ng-eventually/example-notebook@workspace:examples/notebook"], + + "@ng-org/alien-deepsignals": ["@ng-org/alien-deepsignals@0.1.2-alpha.11", "", { "dependencies": { "alien-signals": "2.0.8" } }, "sha512-nPgqOrheAda/pW5FHgSb45SrSZWuyMyEVqO683ijEsVPpD105bngfh92PPfcRoRnFzGSoKXa3CfuqUHi2+qVIQ=="], + + "@ng-org/shex-orm": ["@ng-org/shex-orm@0.1.2-alpha.8", "", { "dependencies": { "@ldo/traverser-shexj": "1.0.0-alpha.28", "@ldo/type-traverser": "1.0.0-alpha.28", "@shexjs/parser": "1.0.0-alpha.28", "commander": "14.0.3", "dts-dom": "3.6.0", "ejs": "3.1.10", "fs-extra": "10.1.0", "jsonld2graphobject": "0.0.5", "loading-cli": "1.1.2", "prettier": "3.9.5" }, "peerDependencies": { "typescript": "5.9.3" }, "bin": { "rdf-orm": "dist/cli.js" } }, "sha512-jogxiLfUtI3WkuF5O3PPwRBRWylpapk/JlvElGUqZnEavXXlEMmEGbK3VwqMUsbp1pweNA0sEKIFLIk3Lzt2Gg=="], + + "@ng-org/web": ["@ng-org/web@0.1.2-alpha.13", "", { "dependencies": { "async-proxy": "0.4.1" } }, "sha512-/xO0c+3NTphnws5Do2LDqgZWmAf+aNnYdChJKdU0dnp1U1iVSgi/y3yb8AYryf0v9sooj0aYJxt08B6DpirFMQ=="], + + "@rdfjs/types": ["@rdfjs/types@1.1.2", "", { "dependencies": { "@types/node": "26.1.1" } }, "sha512-wqpOJK1QCbmsGNtyzYnojPU8gRDPid2JO0Q0kMtb4j65xhCK880cnKAfEOwC+dX85VJcCByQx5zOwyyfCjDJsg=="], + + "@shexjs/parser": ["@shexjs/parser@1.0.0-alpha.28", "", { "dependencies": { "@shexjs/util": "1.0.0-alpha.28", "@ts-jison/parser": "0.4.1-alpha.1" } }, "sha512-eeVeHq/2JG9X+3h7y+7EmuBSWWl2EMj/EQBLk5CTRx4W4hWDdjWczsY8RWwKjkIzLwUS1+G0aiAI1u5LHCZ2Rw=="], + + "@shexjs/term": ["@shexjs/term@1.0.0-alpha.27", "", { "dependencies": { "@types/shexj": "2.1.7", "rdf-data-factory": "1.1.3", "relativize-url": "0.1.0" } }, "sha512-+D7P7pglRPTZC2RkwaQuq+cgBZImx+61JZtcN77uEJVqcGaIscQK5hScsKhAPIo16/I+4jhIUCEFojXqw6otpg=="], + + "@shexjs/util": ["@shexjs/util@1.0.0-alpha.28", "", { "dependencies": { "@shexjs/term": "1.0.0-alpha.27", "@shexjs/visitor": "1.0.0-alpha.27", "@types/shexj": "2.1.7", "hierarchy-closure": "1.2.2", "sync-request": "6.1.0" } }, "sha512-L8pBokTU/5eNRJPkC8R9SIgPw6/JDh/bHKdV5TZzf8/FkOMNJwKIy6UDHXM1I8FJ+c8u2gOOHp2MA+7b+md+0A=="], + + "@shexjs/visitor": ["@shexjs/visitor@1.0.0-alpha.27", "", {}, "sha512-9s67A+f0ZZNw/SNxqoi1483CqUca8dbnHM6WDWsRH4+eXlQpQqwOZDxA8uKEaWeX4VcDrDwzWpr0WvK6EyDWIQ=="], + + "@ts-jison/common": ["@ts-jison/common@0.4.1-alpha.1", "", {}, "sha512-SDbHzq+UMD+V3ciKVBHwCEgVqSeyQPTCjOsd/ZNTGySUVg4x3EauR9ZcEfdVFAsYRR38XWgDI+spq5LDY46KvQ=="], + + "@ts-jison/lexer": ["@ts-jison/lexer@0.4.1-alpha.1", "", { "dependencies": { "@ts-jison/common": "0.4.1-alpha.1" } }, "sha512-5C1Wr+wixAzn2MOFtgy7KbT6N6j9mhmbjAtyvOqZKsikKtNOQj22MM5HxT+ooRexG2NbtxnDSXYdhHR1Lg58ow=="], + + "@ts-jison/parser": ["@ts-jison/parser@0.4.1-alpha.1", "", { "dependencies": { "@ts-jison/common": "0.4.1-alpha.1", "@ts-jison/lexer": "0.4.1-alpha.1" } }, "sha512-xNj+qOez/7dju44LlYiTlCjxMzW5oek9EckUAElfln/GBK9vgMSk0swWcnacMr0TYbGjUQuXvL2wEgmDf5WajQ=="], + + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], + + "@types/concat-stream": ["@types/concat-stream@1.6.1", "", { "dependencies": { "@types/node": "8.10.66" } }, "sha512-eHE4cQPoj6ngxBZMvVf6Hw7Mh4jMW4U9lpGmS5GBPB9RYxlFg+CHaVN7ErNY4W9XfLIEn20b4VDYaIrbq0q4uA=="], + + "@types/form-data": ["@types/form-data@0.0.33", "", { "dependencies": { "@types/node": "8.10.66" } }, "sha512-8BSvG1kGm83cyJITQMZSulnl6QV8jqAGreJsc5tPu1Jq0vTSOiY/k24Wx82JRpWwZSqrala6sd5rWi6aNXvqcw=="], + + "@types/http-link-header": ["@types/http-link-header@1.0.7", "", { "dependencies": { "@types/node": "18.19.130" } }, "sha512-snm5oLckop0K3cTDAiBnZDy6ncx9DJ3mCRDvs42C884MbVYPP74Tiq2hFsSDRTyjK6RyDYDIulPiW23ge+g5Lw=="], + + "@types/jsonld": ["@types/jsonld@1.5.15", "", {}, "sha512-PlAFPZjL+AuGYmwlqwKEL0IMP8M8RexH0NIPGfCVWSQ041H2rR/8OlyZSD7KsCVoN8vCfWdtWDBxX8yBVP+xow=="], + + "@types/node": ["@types/node@26.1.1", "", { "dependencies": { "undici-types": "8.3.0" } }, "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw=="], + + "@types/qs": ["@types/qs@6.15.1", "", {}, "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw=="], + + "@types/shexj": ["@types/shexj@2.1.7", "", {}, "sha512-pu/0vIZxFTMPVjTlo5MJKFkBL/EbAuFhtCXpmBB7ZdUiyNpc6pt8GxfyRPqdf6q2SsWu71a/vbhvGK2IZN2Eug=="], + + "alien-signals": ["alien-signals@2.0.8", "", {}, "sha512-844G1VLkk0Pe2SJjY0J8vp8ADI73IM4KliNu2OGlYzWpO28NexEUvjHTcFjFX3VXoiUtwTbHxLNI9ImkcoBqzA=="], + + "asap": ["asap@2.0.6", "", {}, "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="], + + "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], + + "async-proxy": ["async-proxy@0.4.1", "", { "dependencies": { "object-path-operator": "3.0.0" } }, "sha512-4e+zNtoGL4+cnqib8v169CnKcRfAsAubp2EsjBhAA5jyW7jjI3t36rVvuqLwmhtliwf8JvSnxinE4ecQN+DK4w=="], + + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "brace-expansion": ["brace-expansion@2.1.2", "", { "dependencies": { "balanced-match": "1.0.2" } }, "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA=="], + + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "26.1.1" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "1.3.0", "function-bind": "1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "1.0.2", "get-intrinsic": "1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "caseless": ["caseless@0.12.0", "", {}, "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw=="], + + "colors-cli": ["colors-cli@1.0.33", "", { "bin": { "colors": "bin/colors" } }, "sha512-PWGsmoJFdOB0t+BeHgmtuoRZUQucOLl5ii81NBzOOGVxlgE04muFNHlR5j8i8MKbOPELBl3243AI6lGBTj5ICQ=="], + + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + + "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], + + "concat-stream": ["concat-stream@1.6.2", "", { "dependencies": { "buffer-from": "1.1.2", "inherits": "2.0.4", "readable-stream": "2.3.8", "typedarray": "0.0.6" } }, "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw=="], + + "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], + + "cross-fetch": ["cross-fetch@3.2.0", "", { "dependencies": { "node-fetch": "2.7.0" } }, "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q=="], + + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + + "dts-dom": ["dts-dom@3.6.0", "", {}, "sha512-on5jxTgt+A6r0Zyyz6ZRHXaAO7J1VPnOd6+AmvI1vH440AlAZZNc5rUHzgPuTjGlrVr1rOWQYNl7ZJK6rDohbw=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "1.0.2", "es-errors": "1.3.0", "gopd": "1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "10.9.4" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "1.3.0", "get-intrinsic": "1.3.0", "has-tostringtag": "1.0.2", "hasown": "2.0.4" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + + "filelist": ["filelist@1.0.6", "", { "dependencies": { "minimatch": "5.1.9" } }, "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA=="], + + "form-data": ["form-data@2.5.6", "", { "dependencies": { "asynckit": "0.4.0", "combined-stream": "1.0.8", "es-set-tostringtag": "2.1.0", "hasown": "2.0.4", "mime-types": "2.1.35", "safe-buffer": "5.2.1" } }, "sha512-Ogz/E85h9tlfJzpI6TuFpGcHZFhLrb9Gw8wq9v40CxSCPnv7ahKr6Xgtkn0KYCDQJ8DNn5VoMO8EXr9V5PadyA=="], + + "fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "4.2.11", "jsonfile": "6.2.1", "universalify": "2.0.1" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], + + "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "1.0.2", "es-define-property": "1.0.1", "es-errors": "1.3.0", "es-object-atoms": "1.1.2", "function-bind": "1.1.2", "get-proto": "1.0.1", "gopd": "1.2.0", "has-symbols": "1.1.0", "hasown": "2.0.4", "math-intrinsics": "1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-port": ["get-port@3.2.0", "", {}, "sha512-x5UJKlgeUiNT8nyo/AcnwLnZuZNcSjSw0kogRB+Whd1fjjFq4B1hySFxSFWWSn4mIBzg3sRNUDFYc4g5gjPoLg=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "1.0.1", "es-object-atoms": "1.1.2" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "1.1.0" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], + + "hierarchy-closure": ["hierarchy-closure@1.2.2", "", {}, "sha512-ZqZvsA6HyMqrmm49D3llYA8x8hqdyDDEkaTXcqwyO+fGQlzxoeXws/5ze11M40s4EoTw7GFxdTKIwj5YDOicLQ=="], + + "http-basic": ["http-basic@8.1.3", "", { "dependencies": { "caseless": "0.12.0", "concat-stream": "1.6.2", "http-response-object": "3.0.2", "parse-cache-control": "1.0.1" } }, "sha512-/EcDMwJZh3mABI2NhGfHOGOeOZITqfkEO4p/xK+l3NpyncIHUQBoMvCSF/b5GqvKtySC2srL/GGG3+EtlqlmCw=="], + + "http-link-header": ["http-link-header@1.1.4", "", {}, "sha512-xT3GPW6/ZbGuw4UvwHqErSCEjNUlwbQJuZn9/q5U4WEKfp2kENVCAlousG1zLxHeaQ/ffOHUNpWamvkbBW0eNw=="], + + "http-response-object": ["http-response-object@3.0.2", "", { "dependencies": { "@types/node": "10.17.60" } }, "sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], + + "jake": ["jake@10.9.4", "", { "dependencies": { "async": "3.2.6", "filelist": "1.0.6", "picocolors": "1.1.1" }, "bin": { "jake": "bin/cli.js" } }, "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA=="], + + "jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "2.0.1" }, "optionalDependencies": { "graceful-fs": "4.2.11" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], + + "jsonld-context-parser": ["jsonld-context-parser@2.4.0", "", { "dependencies": { "@types/http-link-header": "1.0.7", "@types/node": "18.19.130", "cross-fetch": "3.2.0", "http-link-header": "1.1.4", "relative-to-absolute-iri": "1.0.8" }, "bin": { "jsonld-context-parse": "bin/jsonld-context-parse.js" } }, "sha512-ZYOfvh525SdPd9ReYY58dxB3E2RUEU4DJ6ZibO8AitcowPeBH4L5rCAitE2om5G1P+HMEgYEYEr4EZKbVN4tpA=="], + + "jsonld2graphobject": ["jsonld2graphobject@0.0.5", "", { "dependencies": { "@rdfjs/types": "1.1.2", "@types/jsonld": "1.5.15", "jsonld-context-parser": "2.4.0", "uuid": "8.3.2" } }, "sha512-5BqfXOq96+OBjjiJNG8gQH66pYt6hW88z2SJxdvFJo4XNoVMvqAcUz+JSm/KEWS5NLRnebApEzFrYP3HUiUmYw=="], + + "loading-cli": ["loading-cli@1.1.2", "", { "dependencies": { "colors-cli": "1.0.33" } }, "sha512-M1ntfXHpdGoQxfaqKBOQPwSrTr9EIoTgj664Q9UVSbSnJvAFdribo+Ij//1jvACgrGHaTvfKoD9PG3NOxGj44g=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "2.1.2" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], + + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "5.0.0" } }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "object-path-operator": ["object-path-operator@3.0.0", "", {}, "sha512-Z7dlPUeXqRU/lLfGerP24dPC66n7ehyXaTM81k71EFlsaaEjOHkf4/uq1WGicfGfiO7snYShneE1YZZUkyRiLQ=="], + + "parse-cache-control": ["parse-cache-control@1.0.1", "", {}, "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "playwright": ["playwright@1.61.1", "", { "dependencies": { "playwright-core": "1.61.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ=="], + + "playwright-core": ["playwright-core@1.61.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg=="], + + "prettier": ["prettier@3.9.5", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg=="], + + "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], + + "promise": ["promise@8.3.0", "", { "dependencies": { "asap": "2.0.6" } }, "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg=="], + + "qs": ["qs@6.15.3", "", { "dependencies": { "es-define-property": "1.0.1", "side-channel": "1.1.1" } }, "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A=="], + + "rdf-data-factory": ["rdf-data-factory@1.1.3", "", { "dependencies": { "@rdfjs/types": "1.1.2" } }, "sha512-ny6CI7m2bq4lfQQmDYvcb2l1F9KtGwz9chipX4oWu2aAtVoXjb7k3d8J1EsgAsEbMXnBipB/iuRen5H2fwRWWQ=="], + + "readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "1.0.3", "inherits": "2.0.4", "isarray": "1.0.0", "process-nextick-args": "2.0.1", "safe-buffer": "5.1.2", "string_decoder": "1.1.1", "util-deprecate": "1.0.2" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + + "relative-to-absolute-iri": ["relative-to-absolute-iri@1.0.8", "", {}, "sha512-U1TmhrhCmXKkDL9mI8gBbF5TN6TKcuv28k5+H3gMCAjoz0TyyHAICHlaGDZsTEBSu2Y3HhDKc8e6X9n33qeIqA=="], + + "relativize-url": ["relativize-url@0.1.0", "", {}, "sha512-YXet4a9wQP96Ru9MQSfoRUzsCaeboLPXj+rVG1ulH4t54zqFHiNmW6FPl7V2dTxk9uHlW3yb9+1jWO44AdWisw=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "side-channel": ["side-channel@1.1.1", "", { "dependencies": { "es-errors": "1.3.0", "object-inspect": "1.13.4", "side-channel-list": "1.0.1", "side-channel-map": "1.0.1", "side-channel-weakmap": "1.0.2" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="], + + "side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "1.3.0", "object-inspect": "1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "1.0.4", "es-errors": "1.3.0", "get-intrinsic": "1.3.0", "object-inspect": "1.13.4" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "1.0.4", "es-errors": "1.3.0", "get-intrinsic": "1.3.0", "object-inspect": "1.13.4", "side-channel-map": "1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "5.1.2" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + + "sync-request": ["sync-request@6.1.0", "", { "dependencies": { "http-response-object": "3.0.2", "sync-rpc": "1.3.7", "then-request": "6.0.2" } }, "sha512-8fjNkrNlNCrVc/av+Jn+xxqfCjYaBoHqCsDz6mt030UMxJGr+GSfCV1dQt2gRtlL63+VPidwDVLr7V2OcTSdRw=="], + + "sync-rpc": ["sync-rpc@1.3.7", "", { "dependencies": { "get-port": "3.2.0" } }, "sha512-YHciI7TUxL8EPqz/bg01sZfwuzQA0odao1wf1Ywdtw7j5vl30aQ6s+bLRTvgPPgzr94cg+WMm6Bxi/P7BJOxgw=="], + + "then-request": ["then-request@6.0.2", "", { "dependencies": { "@types/concat-stream": "1.6.1", "@types/form-data": "0.0.33", "@types/node": "8.10.66", "@types/qs": "6.15.1", "caseless": "0.12.0", "concat-stream": "1.6.2", "form-data": "2.5.6", "http-basic": "8.1.3", "http-response-object": "3.0.2", "promise": "8.3.0", "qs": "6.15.3" } }, "sha512-3ZBiG7JvP3wbDzA9iNY5zJQcHL4jn/0BWtXIkagfz7QgOL/LqjCEOBQuJNZfu0XYnv5JhKh+cDxCPM4ILrqruA=="], + + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + + "typedarray": ["typedarray@0.0.6", "", {}, "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], + + "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "0.0.3", "webidl-conversions": "3.0.1" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + + "@types/concat-stream/@types/node": ["@types/node@8.10.66", "", {}, "sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw=="], + + "@types/form-data/@types/node": ["@types/node@8.10.66", "", {}, "sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw=="], + + "@types/http-link-header/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "5.26.5" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + + "http-response-object/@types/node": ["@types/node@10.17.60", "", {}, "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw=="], + + "jsonld-context-parser/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "5.26.5" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + + "readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + + "string_decoder/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + + "then-request/@types/node": ["@types/node@8.10.66", "", {}, "sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw=="], + + "@types/http-link-header/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "jsonld-context-parser/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + } +} diff --git a/examples/notebook/README.md b/examples/notebook/README.md new file mode 100644 index 0000000..50c5194 --- /dev/null +++ b/examples/notebook/README.md @@ -0,0 +1,19 @@ +# Notebook — the library's example application + +A minimal application written against `@ng-eventually/client`, in plain DOM. + +It exists for two reasons, and the second is the one that matters. + +**It shows how to use the library.** Every call in `app.ts` is what a real consumer writes. There is no test scaffolding, no privileged import, no reaching into the library's internals — it resolves `@ng-eventually/client` as an external consumer does. If something reads awkwardly here, it reads awkwardly for everyone. + +**It is what the e2e suite drives.** The suite used to talk to a bag of methods on `window.__sdk`, which proved the functions ran but never that an application could be written with them. That gap shipped a real defect: a document's inbox was green in tests and unusable in practice, because the harness handed an address across an identity boundary through a variable — something no application can do. This app can only do what an application can do. + +It has already paid for itself twice: writing it surfaced that `UnionSubject` returned `string` where the values are always document references (so a consumer had to cast whatever it had just read before passing it back), and that the access gate normalized what a user typed but not what the URL carried. + +## What it exercises + +Signing in, writing notes by scope, listing one's own, reading a note received as a link, handing a reader the key to a private note, opening a note for messages, leaving a message on someone else's note, and reacting to changes. + +## Running it + +The e2e suite builds and serves it (`packages/client/e2e/`). To open it by hand you need a wallet: serve the folder with a bundled `app.js` and a `/shared-wallet.ngw`, and set `__NOTEBOOK_WALLET_PASSWORD__`. diff --git a/examples/notebook/app.ts b/examples/notebook/app.ts new file mode 100644 index 0000000..b34d776 --- /dev/null +++ b/examples/notebook/app.ts @@ -0,0 +1,255 @@ +/** + * Notebook — a minimal application written against `@ng-eventually/client`. + * + * It exists for two reasons, and the second is the one that matters: + * + * 1. **It shows how to use the library.** Every call here is what a real consumer + * writes; there is no test scaffolding, no privileged import, no reaching into the + * library's internals. If something is awkward here, it is awkward for everyone. + * + * 2. **It is what the e2e suite drives.** The suite used to talk to a bag of methods on + * `window.__sdk`, which proved the functions ran but never that an application could + * be written with them — and that gap shipped a real defect: a document's inbox was + * green in tests and unusable in practice, because the harness handed the address + * across an identity boundary through a variable. No application can do that. This + * app can only do what an application can do, so a test that passes here means the + * surface is usable, not merely callable. + * + * ── The domain is deliberately thin ─────────────────────────────────────── + * Notes. Each user writes their own, may publish one, may hand a reader the key to a + * private one, and may leave a message on someone else's note. That is enough to + * exercise placement by scope, capability possession, directed sharing, per-document + * inboxes and reactive reads — without inventing a product. + * + * ── Plain DOM, on purpose ───────────────────────────────────────────────── + * The library imposes no framework, so its example must not adopt one: a consumer + * reading this should see the SDK calls, not a component tree. The UI here is the + * shortest thing that makes each act reachable. + */ + +import { + docs, + ensureIdentity, + inbox, + isNuri, + readUnion, + storeRegistry, + subscribeDoc, + type Nuri, + type Scope, +} from "@ng-eventually/client"; +import { capFor, configure, configureStoreRegistry, setCurrentUser } from "@ng-eventually/client/polyfill"; +import { ng as realNg, init as realInit } from "@ng-org/web"; + +// --- the domain, such as it is --------------------------------------------- + +const TITLE = "urn:notebook:title"; +const BODY = "urn:notebook:body"; + +interface Note { + doc: Nuri; + title: string; + body: string; +} + +// --- bootstrap: the two polyfill-era calls --------------------------------- +// +// Everything else an application calls is SDK surface, preserved at migration. These +// two are the scaffolding: `configure` becomes inert (the app will import the real SDK) +// and the identity will come from the wallet instead of a barrier. + +let session: { session_id: string } | null = null; +const sessionReady = new Promise<{ session_id: string }>((resolve) => { + realInit((event: { status: string; session?: { session_id: string } }) => { + if (event.status === "loggedin" && event.session) { + session = event.session; + resolve(event.session); + } + }, true, []); +}); + +configure({ + ng: realNg, + useShape: (() => {}) as never, // this example reads through `readUnion`, not the ORM + init: realInit, + sharedWallet: { + fileUrl: "/shared-wallet.ngw", + password: (globalThis as { __NOTEBOOK_WALLET_PASSWORD__?: string }).__NOTEBOOK_WALLET_PASSWORD__ ?? "", + }, +}); + +configureStoreRegistry({ + getSession: async () => { + const s = session ?? (await sessionReady); + return { + sessionId: s.session_id, + privateStoreId: (s as Record).private_store_id!, + protectedStoreId: (s as Record).protected_store_id, + publicStoreId: (s as Record).public_store_id, + }; + }, + normalizeId: (id) => id.trim().replace(/^@/, "").toLowerCase(), +}); + +// --- the acts --------------------------------------------------------------- + +/** Write a new note in `scope`. The document is created, then filled. */ +async function writeNote(scope: Scope, title: string, body: string): Promise { + const me = currentIdentity(); + const doc = await storeRegistry.createEntityDoc(me, scope); + const s = await sessionReady; + await docs.sparqlUpdate( + s.session_id, + `INSERT DATA { <${doc}> <${TITLE}> "${escape(title)}" ; <${BODY}> "${escape(body)}" }`, + doc, + ); + return doc; +} + +/** My notes in `scope`, read the way the library intends: list, then read. */ +async function myNotes(scope: Scope): Promise { + const docsOfScope = await storeRegistry.listMyEntityDocs(currentIdentity(), scope); + const subjects = await readUnion(docsOfScope); + return subjects.map((s) => ({ + doc: s.subject, + title: s.props[TITLE]?.[0] ?? "(sans titre)", + body: s.props[BODY]?.[0] ?? "", + })); +} + +/** + * Read someone else's note from its link. + * + * The link is what circulates in this model — you do not discover a note, you are given + * its link. `hasReadCap` is the door an untrusted string goes through. + */ +async function readSharedNote(link: string): Promise { + if (!isNuri(link)) return null; + const [note] = await readUnion([link]); + if (!note) return null; + return { + doc: note.subject, + title: note.props[TITLE]?.[0] ?? "(sans titre)", + body: note.props[BODY]?.[0] ?? "", + }; +} + +/** Hand a reader the key to one of my notes, addressed to their inbox. */ +async function shareNote(doc: Nuri, withUser: string): Promise { + const cap = capFor(doc); + if (!cap) throw new Error("this note is not mine to share"); + const theirInbox = await storeRegistry.userInbox(withUser, "protected"); + await inbox.shareCap(cap, theirInbox); +} + +/** Open a note for messages — only its owner can, and only they will read them. */ +async function openNoteForMessages(doc: Nuri): Promise { + await storeRegistry.openDocumentInbox(doc); +} + +/** Leave a message on someone else's note. One call, naming the NOTE. */ +async function leaveMessage(doc: Nuri, text: string): Promise { + await inbox.postToDocument(doc, { payload: { text } }); +} + +/** The messages left on one of my notes. */ +async function messagesOn(doc: Nuri): Promise { + const address = await storeRegistry.documentInboxAddress(doc); + if (!address) return []; + const deposits = await inbox.read(address); + return deposits.map((d) => String((d.payload as { text?: string })?.text ?? "")); +} + +/** Re-render whenever a note changes — locally or from a peer. */ +function watchNote(doc: Nuri, onChange: () => void): () => void { + return subscribeDoc(doc, onChange); +} + +// --- identity --------------------------------------------------------------- + +let identity = ""; +function currentIdentity(): string { + if (!identity) throw new Error("not signed in yet"); + return identity; +} + +/** + * Sign in. The library shows its access barrier when it needs one; the day the wallet + * supplies the identity, this resolves silently and nothing here changes. + */ +async function signIn(): Promise { + await ensureIdentity(); + await sessionReady; + identity = readIdentityBack(); +} + +/** The library owns the identity; the app asks for it rather than remembering it. */ +function readIdentityBack(): string { + return new URLSearchParams(location.search).get("ng-id") ?? localStorage.getItem("ng-eventually:identity") ?? ""; +} + +function escape(s: string): string { + return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n"); +} + +// --- the thinnest UI that makes each act reachable -------------------------- + +const el = (id: string): HTMLElement => document.getElementById(id)!; +const val = (id: string): string => (el(id) as HTMLInputElement).value.trim(); + +async function refresh(): Promise { + const scope = (el("scope") as HTMLSelectElement).value as Scope; + const notes = await myNotes(scope); + el("notes").innerHTML = notes + .map( + (n) => `
  • + ${n.title}${n.body} + + + +
  • `, + ) + .join(""); + el("who").textContent = identity; +} + +function wire(): void { + el("write").addEventListener("click", async () => { + await writeNote((el("scope") as HTMLSelectElement).value as Scope, val("title"), val("body")); + await refresh(); + }); + el("openLink").addEventListener("click", async () => { + const note = await readSharedNote(val("link")); + el("shared").textContent = note ? `${note.title} — ${note.body}` : "(illisible)"; + }); + el("leave").addEventListener("click", async () => { + await leaveMessage(val("onNote") as Nuri, val("message")); + el("left").textContent = "déposé"; + }); + el("notes").addEventListener("click", async (e) => { + const target = e.target as HTMLElement; + const doc = target.dataset.doc as Nuri | undefined; + if (!doc) return; + if (target.classList.contains("share")) { + await shareNote(doc, val("shareWith")); + el("shareResult").textContent = "partagé"; + } else if (target.classList.contains("open")) { + await openNoteForMessages(doc); + el("shareResult").textContent = "ouverte aux messages"; + } else if (target.classList.contains("msgs")) { + el("messages").textContent = (await messagesOn(doc)).join(" | ") || "(aucun)"; + } + }); +} + +async function main(): Promise { + wire(); + await signIn(); + await refresh(); +} + +void main(); + +// The e2e suite drives this app through the DOM. It exposes nothing else: a test that +// needed a back door would be testing something an application cannot do. +(globalThis as { __notebook?: unknown }).__notebook = { watchNote, setCurrentUser }; diff --git a/examples/notebook/index.html b/examples/notebook/index.html new file mode 100644 index 0000000..c18e947 --- /dev/null +++ b/examples/notebook/index.html @@ -0,0 +1,58 @@ + + + + + Notebook — exemple ng-eventually + + + +

    Connecté :

    + +
    + Écrire une note + + + + +
    + +
    + Mes notes + +
      +

      +

      +
      + +
      + Ouvrir une note reçue + + +

      +
      + +
      + Laisser un message sur la note d'un autre + + + +

      +
      + + + + diff --git a/examples/notebook/package.json b/examples/notebook/package.json new file mode 100644 index 0000000..ebea866 --- /dev/null +++ b/examples/notebook/package.json @@ -0,0 +1,10 @@ +{ + "name": "@ng-eventually/example-notebook", + "private": true, + "type": "module", + "description": "A minimal application written against @ng-eventually/client — the library's usage example, and what the e2e suite drives.", + "dependencies": { + "@ng-eventually/client": "workspace:*", + "@ng-org/web": "0.1.2-alpha.13" + } +} diff --git a/examples/notebook/tsconfig.json b/examples/notebook/tsconfig.json new file mode 100644 index 0000000..e8809a9 --- /dev/null +++ b/examples/notebook/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "types": [ + "bun" + ], + "noEmit": true, + "lib": [ + "ESNext", + "DOM" + ], + "paths": { + "@ng-eventually/client": [ + "../../packages/client/src/index.ts" + ], + "@ng-eventually/client/polyfill": [ + "../../packages/client/src/polyfill.ts" + ], + "@ng-org/web": [ + "../../node_modules/.bun/@ng-org+web@0.1.2-alpha.13/node_modules/@ng-org/web/dist/index.d.ts" + ] + } + }, + "include": [ + "." + ] +} diff --git a/package.json b/package.json index 5d22070..0bf8717 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,10 @@ "private": true, "type": "module", "description": "Generic polyfill layer over the NextGraph JS SDK — makes a single broker behave like the (not-yet-shipped) multi-user NextGraph: emulated capabilities and inbox. Drop-in SDK-shaped client; remove at migration. (A global-index curator package is deferred — see README.)", - "workspaces": ["packages/*"], + "workspaces": [ + "packages/*", + "examples/*" + ], "scripts": { "test": "bun test", "typecheck": "bunx tsc --noEmit -p packages/client/tsconfig.json" diff --git a/packages/client/e2e/run.ts b/packages/client/e2e/run.ts index e2b6786..2785b2c 100644 --- a/packages/client/e2e/run.ts +++ b/packages/client/e2e/run.ts @@ -120,6 +120,21 @@ async function main(): Promise { const info = await sdkGet(frame, "sessionInfo"); check("broker session connected", info?.session_id !== undefined && info?.session_id !== null, `session=${JSON.stringify(info)}`); + // ── access gate ───────────────────────────────────────────────────────── + console.log("\n── access gate ──"); + await step("the gate asks on a first access, and settles the identity normalized", async () => { + const r = await sdk(frame, "accessGateFirstVisit", "@Erin"); + check( + "barrier shown, Entrer disabled while empty, identity normalized, barrier removed", + r.shown === true && r.disabledWhenEmpty === true && r.identity === "erin" && r.stillMounted === false, + `shown=${r.shown} disabledWhenEmpty=${r.disabledWhenEmpty} identity=${r.identity} stillMounted=${r.stillMounted}`, + ); + }); + await step("the gate stays away when the identity is already known", async () => { + const r = await sdk(frame, "accessGateReturningVisit", "erin"); + check("no barrier for a returning user", r.shown === false && r.identity === "erin", `shown=${r.shown}`); + }); + // ── docs primitives ───────────────────────────────────────────────────── console.log("\n── docs primitives ──"); await step("docCreate returns a usable NURI", async () => { diff --git a/packages/client/e2e/sdk-entry.ts b/packages/client/e2e/sdk-entry.ts index b6b1d67..bfa8053 100644 --- a/packages/client/e2e/sdk-entry.ts +++ b/packages/client/e2e/sdk-entry.ts @@ -19,6 +19,7 @@ import { configure, configureStoreRegistry, setCurrentUser, + getCurrentUser, capFor, getCaps, resetCaps, @@ -40,7 +41,7 @@ import { // `storeRegistry` above is the app-facing slice; these are the shim internals. import * as registryInternals from "../src/shared-wallet/account-registry"; import * as virtualUsers from "../src/shared-wallet/virtual-users"; -import { isNuri } from "@ng-eventually/client"; +import { isNuri, ensureIdentity } from "@ng-eventually/client"; import type { Nuri, ShapeObservable, ShapeQuery } from "@ng-eventually/client"; const { IdentityStore } = virtualUsers; @@ -96,6 +97,10 @@ configure({ ng: realNg, useShape: fakeUseShape, init: realInit, + // The harness already holds an open wallet (the suite imports it itself), so it never + // needs the gate's assisted import. These values exist only so the gate has something + // to render when the access-gate steps exercise it — they are never used to import. + sharedWallet: { fileUrl: "/harness-not-used.ngw", password: "harness" }, }); configureStoreRegistry({ @@ -167,6 +172,50 @@ const identity = new IdentityStore( return { walletName, b64: btoa(bin), len: bytes.length }; }, + /** + * THE ACCESS GATE, in a real browser — ported from the consumer's + * `barriere-acces-identifiant` feature, which the library took over with the flow. + * + * Unit tests pin the resolution ORDER (`test/access-gate.test.ts`); only a real DOM can + * pin the barrier itself: that it appears on a first access, that entering a value + * settles the identity normalized, and — the one that matters most — that it does NOT + * appear when the identity is already known, since a returning user seeing the barrier + * again is the visible face of the silent bug (a second virtual space). + */ + async accessGateFirstVisit(raw: string) { + setCurrentUser(null); + try { window.localStorage.removeItem("ng-eventually:identity"); } catch {} + const done = ensureIdentity(); + const gate = document.querySelector('[data-ng-eventually="access-gate"]'); + const root = gate?.shadowRoot ?? null; + const input = root?.querySelector("input") as HTMLInputElement | null; + const button = root?.querySelector("button.go") as HTMLButtonElement | null; + const shown = input !== null && button !== null; + const disabledWhenEmpty = button?.disabled ?? null; + if (input && button) { + input.value = raw; + input.dispatchEvent(new Event("input")); + button.click(); + } + await done; + return { + shown, + disabledWhenEmpty, + identity: getCurrentUser(), + stillMounted: document.querySelector('[data-ng-eventually="access-gate"]') !== null, + }; + }, + + /** The barrier must stay away once an identity is known. */ + async accessGateReturningVisit(known: string) { + setCurrentUser(known); + await ensureIdentity(); + return { + shown: document.querySelector('[data-ng-eventually="access-gate"]') !== null, + identity: getCurrentUser(), + }; + }, + // ── docs primitives ────────────────────────────────────────────────────── async docCreate() { const s = await sessionReady; diff --git a/packages/client/src/shared-wallet/access-gate.ts b/packages/client/src/shared-wallet/access-gate.ts index a396f6b..c23b207 100644 --- a/packages/client/src/shared-wallet/access-gate.ts +++ b/packages/client/src/shared-wallet/access-gate.ts @@ -39,7 +39,32 @@ * a second virtual user, and the returning user silently lands in an empty space. */ -import { getConfig, getCurrentUser, setCurrentUser } from "./bootstrap"; +import { + getConfig, + getCurrentUser, + getStoreRegistryDeps, + setCurrentUser, +} from "./bootstrap"; + +/** + * Normalize an identifier the SAME way the shim keys accounts on. + * + * Not a detail: the identifier arrives from three places — typed at the gate, read from + * the URL after the broker round-trip, read from storage — and if any of them normalizes + * differently, that path keys onto a DIFFERENT virtual user. `@Erin` from the URL and + * `erin` typed at the gate must be one space, not two. So there is one normalizer, the + * injected one, and the gate borrows it rather than keeping its own `toLowerCase()`. + * + * Falls back to the library's own default when the registry is not configured yet, which + * is possible since the gate can run before anything else. + */ +function normalizeIdentity(raw: string): string { + try { + return getStoreRegistryDeps().normalizeId(raw); + } catch { + return raw.trim().replace(/^@/, "").toLowerCase(); + } +} /** Where the gate stashes the identifier so a plain reload prefills it. */ const STORAGE_KEY = "ng-eventually:identity"; @@ -50,6 +75,11 @@ const URL_PARAM = "ng-id"; * What a deployment must supply for the gate to run. These are not settings a user * tunes: they are the shared wallet this deployment hands out, so they belong to * whoever deploys, and they disappear with the gate. + * + * **The library reads no environment variable, ever.** The application resolves these at + * its own build — copying the `.ngw` into its bundle, injecting the password — and + * passes the VALUES here. A library that read `process.env` would impose its build + * system on every consumer, and would be untestable with other values. */ export interface SharedWalletConfig { /** URL of the `.ngw` file served by the application's own bundle. */ @@ -58,8 +88,6 @@ export interface SharedWalletConfig { password: string; /** The wallet app where the import happens. Defaults to the public one. */ importUrl?: string; - /** Shown as the gate's heading. The deployment's name, not a domain concept. */ - appName?: string; } const DEFAULT_IMPORT_URL = "https://nextgraph.eu/#/wallet/login"; @@ -69,11 +97,15 @@ function storedIdentity(): string | null { try { const fromUrl = new URLSearchParams(globalThis.location?.search ?? "").get(URL_PARAM); if (fromUrl && fromUrl.trim()) { - // Persist it in THIS partition too, so a later reload without the param prefills. - globalThis.localStorage?.setItem(STORAGE_KEY, fromUrl.trim()); - return fromUrl.trim(); + // Normalized on the way IN: the URL carries whatever a user or a link put there + // (`@Erin`), and an un-normalized value keys onto a different virtual user than the + // same identifier typed at the gate. + const normalized = normalizeIdentity(fromUrl); + globalThis.localStorage?.setItem(STORAGE_KEY, normalized); + return normalized; } - return globalThis.localStorage?.getItem(STORAGE_KEY) ?? null; + const stored = globalThis.localStorage?.getItem(STORAGE_KEY); + return stored ? normalizeIdentity(stored) : null; } catch { return null; // storage blocked (private mode, sandboxed iframe) — the gate asks again } @@ -95,11 +127,17 @@ function rememberIdentity(id: string): void { /** * Show the gate and resolve with the identifier the user entered. * + * No prefill parameter, deliberately: the gate is shown ONLY when no identity is known, + * so there is never a value to prefill. The consumer this was moved from did prefill, + * because its screen reappeared after the broker round-trip — here the URL carries the + * identity across that round-trip, so a returning user does not see the barrier at all. + * The need is met one level up rather than papered over in the form. + * * Deliberately plain DOM: this is a technical barrier shown before an application * renders, like a password prompt on a closed beta. Binding it to a UI framework would * make every consumer adopt that framework for a screen that is going away. */ -function askForIdentity(cfg: SharedWalletConfig, prefill: string | null): Promise { +function askForIdentity(cfg: SharedWalletConfig): Promise { const importUrl = cfg.importUrl ?? DEFAULT_IMPORT_URL; return new Promise((resolve) => { const host = document.createElement("div"); @@ -130,8 +168,8 @@ function askForIdentity(cfg: SharedWalletConfig, prefill: string | null): Promis .hint { color: #999; font-size: 12px; margin: 6px 0 0; }
      -

      ${cfg.appName ?? "Accès"}

      -

      Espace de test

      +

      Accès

      +

      Environnement de test

      1
      Télécharger le portefeuille
      Télécharger le fichier @@ -164,7 +202,6 @@ function askForIdentity(cfg: SharedWalletConfig, prefill: string | null): Promis input.addEventListener("input", sync); input.addEventListener("keydown", (e) => { if ((e as KeyboardEvent).key === "Enter") enter(); }); go.addEventListener("click", enter); - if (prefill) { input.value = prefill; } sync(); document.body.appendChild(host); @@ -208,8 +245,8 @@ export async function ensureIdentity(): Promise { ); } - const chosen = await askForIdentity(cfg, null); - const normalized = chosen.trim().toLowerCase(); + const chosen = await askForIdentity(cfg); + const normalized = normalizeIdentity(chosen); rememberIdentity(normalized); setCurrentUser(normalized); } diff --git a/packages/client/src/surface/read-model.ts b/packages/client/src/surface/read-model.ts index ad6faa8..8f37b1d 100644 --- a/packages/client/src/surface/read-model.ts +++ b/packages/client/src/surface/read-model.ts @@ -57,10 +57,19 @@ void sparqlUpdate; /** One subject read from a doc, with its properties (predicate → values). */ export interface UnionSubject { - /** The subject IRI (`?s`) — in the polyfill, the doc's own NURI. */ - subject: string; + /** + * The subject IRI (`?s`) — in the polyfill, the doc's own NURI. + * + * Typed `Nuri`, not `string`: both fields are always document references here (the + * read is anchored per document and the subject is pinned to the anchor), and typing + * them loosely forced a consumer to cast whatever it had just read before it could + * pass it back — `shareNote(note.doc)`, `leaveMessage(note.doc)`. A cast at that + * boundary re-opens exactly the confusion the template literal types exist to close. + * Found by writing the example application (`examples/notebook`). + */ + subject: Nuri; /** The graph (doc NURI) the subject was read from. */ - graph: string; + graph: Nuri; /** predicate IRI → the list of object values (literals or IRIs) for it. */ props: Record; } diff --git a/packages/client/test/access-gate.test.ts b/packages/client/test/access-gate.test.ts index 11cdd21..e6ed38b 100644 --- a/packages/client/test/access-gate.test.ts +++ b/packages/client/test/access-gate.test.ts @@ -6,7 +6,14 @@ * lands in an empty space with no error anywhere. So the order is pinned, not trusted. */ import { test, expect, afterEach } from "bun:test"; -import { configure, resetConfig, setCurrentUser, getCurrentUser } from "../src/polyfill"; +import { + configure, + configureStoreRegistry, + resetConfig, + resetStoreRegistry, + setCurrentUser, + getCurrentUser, +} from "../src/polyfill"; import { ensureIdentity } from "../src/shared-wallet/access-gate"; const KEY = "ng-eventually:identity"; @@ -32,12 +39,17 @@ function inPage(search: string, storage: ReturnType) { afterEach(() => { setCurrentUser(null); resetConfig(); + resetStoreRegistry(); delete (globalThis as any).location; delete (globalThis as any).localStorage; delete (globalThis as any).history; }); function configured() { + configureStoreRegistry({ + getSession: async () => ({ sessionId: "s", privateStoreId: "did:ng:o:p" }), + normalizeId: (id: string) => id.trim().replace(/^@/, "").toLowerCase(), + }); configure({ ng: {} as never, useShape: (() => {}) as never, @@ -92,3 +104,21 @@ test("no shared wallet configured → it refuses, rather than inventing a space" inPage("", fakeStorage()); await expect(ensureIdentity()).rejects.toThrow(/no shared wallet configured/i); }); + +test("the URL value is NORMALIZED on the way in — `@Erin` and `erin` are one space", async () => { + // Ported from the consumer's `identifiant-resolution` feature, and it caught a real + // defect here: the gate normalized what a user TYPED but not what the URL carried, so + // a link with `?ng-id=@Erin` keyed onto a different virtual user than the same person + // typing `erin`. One normalizer — the injected one — for all three entry paths. + configured(); + inPage("?ng-id=@Erin", fakeStorage()); + await ensureIdentity(); + expect(getCurrentUser()).toBe("erin"); +}); + +test("a stored value is normalized too — an old entry cannot key onto a second space", async () => { + configured(); + inPage("", fakeStorage({ [KEY]: "@Frank" })); + await ensureIdentity(); + expect(getCurrentUser()).toBe("frank"); +});